Explorar el Código

更新服务器结构体名称和相关方法

将所有涉及 `HTTPServer` 的方法和字段更新为 `HuanProxyServer`,并调整了相关的初始化和运行逻辑。同时,增加了 HTTPS 支持和证书管理功能。
SongZihuan hace 3 meses
padre
commit
4d3ca76c9e

+ 17 - 1
go.mod

@@ -1,17 +1,33 @@
 module github.com/SongZihuan/huan-proxy
 
-go 1.21.0
+go 1.22.0
 
 toolchain go1.23.2
 
 require (
 	github.com/fsnotify/fsnotify v1.8.0
 	github.com/gabriel-vasile/mimetype v1.4.7
+	github.com/go-acme/lego/v4 v4.21.0
 	github.com/mattn/go-isatty v0.0.20
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
+	github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect
+	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/json-iterator/go v1.1.12 // 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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // 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
+	golang.org/x/tools v0.28.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
 )

+ 112 - 1
go.sum

@@ -1,15 +1,126 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
+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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
 github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
+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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+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/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 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=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
+github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
+github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
+github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+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/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+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.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 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=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+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/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 170 - 0
src/certssl/account/data.go

@@ -0,0 +1,170 @@
+package account
+
+import (
+	"crypto"
+	"encoding/json"
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/utils"
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+	"os"
+	"path"
+	"time"
+)
+
+const DefaultAccountExp = 24 * time.Hour
+const DefaultUserKeyType = certcrypto.RSA4096
+
+var ErrExpiredAccount = fmt.Errorf("account expired")
+var ErrNotValidAccount = fmt.Errorf("account not valid")
+var user *Account
+
+type Data struct {
+	Resource       *registration.Resource `json:"resource,omitempty"`
+	Email          string                 `json:"email,omitempty"`
+	RegisterTime   int64                  `json:"register-time,omitempty"`
+	ExpirationTime int64                  `json:"expiration-time,omitempty"`
+}
+
+// Account 不得包含指针
+type Account struct {
+	data        Data
+	key         crypto.PrivateKey
+	dir         string
+	accountpath string
+	keypath     string
+}
+
+func NewAccount(basedir string, email string) (*Account, error) {
+	dir := path.Join(basedir, "account", email)
+	err := os.MkdirAll(dir, 0775)
+	if err != nil {
+		return nil, fmt.Errorf("create account dir failed: %s", err.Error())
+	}
+
+	privateKey, err := certcrypto.GeneratePrivateKey(DefaultUserKeyType)
+	if err != nil {
+		return nil, fmt.Errorf("generate new user private key failed: %s", err.Error())
+	}
+
+	now := time.Now()
+	user = &Account{
+		data: Data{
+			Email:          email,
+			Resource:       nil,
+			RegisterTime:   now.Unix(),
+			ExpirationTime: now.Add(DefaultAccountExp).Unix(),
+		},
+		key:         privateKey,
+		dir:         dir,
+		accountpath: path.Join(dir, "account.json"),
+		keypath:     path.Join(dir, "account.key"),
+	}
+	return user, nil
+}
+
+func LoadAccount(basedir string, email string) (*Account, error) {
+	if user != nil {
+		return user, nil
+	}
+
+	dir := path.Join(basedir, "account", email)
+	accountpath := path.Join(dir, "account.json")
+	keypath := path.Join(dir, "account.key")
+
+	dataAccount, err := os.ReadFile(accountpath)
+	if err != nil {
+		return nil, fmt.Errorf("read account file failed: %s", err.Error())
+	}
+
+	var data Data
+	err = json.Unmarshal(dataAccount, &data)
+	if err != nil {
+		return nil, fmt.Errorf("load account error")
+	}
+
+	dataKey, err := os.ReadFile(keypath)
+	if err != nil {
+		return nil, fmt.Errorf("read account key file failed: %s", err.Error())
+	}
+
+	privateKey, err := utils.ReadPrivateKey(dataKey)
+	if err != nil {
+		return nil, fmt.Errorf("read account key failed: %s", err.Error())
+	}
+
+	if time.Now().After(time.Unix(data.ExpirationTime, 0)) {
+		return nil, ErrExpiredAccount
+	}
+
+	if data.Resource == nil || data.Resource.Body.Status != "valid" {
+		return nil, ErrNotValidAccount
+	}
+
+	user = &Account{
+		data:        data,
+		key:         privateKey,
+		dir:         dir,
+		accountpath: accountpath,
+		keypath:     keypath,
+	}
+	return user, nil
+}
+
+func (u *Account) GetEmail() string {
+	return u.data.Email
+}
+
+func (u *Account) GetRegistration() *registration.Resource {
+	return u.data.Resource
+}
+
+func (u *Account) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+func (u *Account) SaveAccount() error {
+	err := os.MkdirAll(u.dir, 0775)
+	if err != nil {
+		return fmt.Errorf("create account dir failed: %s", err.Error())
+	}
+
+	data, err := json.Marshal(u.data)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(u.accountpath, data, 0644)
+	if err != nil {
+		return fmt.Errorf("failed to write account %s: %s", u.accountpath, err.Error())
+	}
+
+	privateKeyData, err := utils.EncodePrivateKeyToPEM(u.key)
+	if err != nil {
+		return fmt.Errorf("failed to read account private %s: %s", u.accountpath, err.Error())
+	}
+
+	err = os.WriteFile(u.keypath, privateKeyData, 0644)
+	if err != nil {
+		return fmt.Errorf("failed to write account %s: %s", u.keypath, err.Error())
+	}
+
+	return nil
+}
+
+func (u *Account) Register(client *lego.Client) (*registration.Resource, error) {
+	if u.data.Resource != nil {
+		return u.data.Resource, nil
+	}
+
+	res, err := register(client)
+	if err != nil {
+		return nil, fmt.Errorf("new account failed: %s", err.Error())
+	} else if res == nil {
+		return nil, fmt.Errorf("new account failed: register return nil, unknown error")
+	}
+
+	u.data.Resource = res
+	return u.data.Resource, nil
+}

+ 20 - 0
src/certssl/account/register.go

@@ -0,0 +1,20 @@
+package account
+
+import (
+	"fmt"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+)
+
+func register(client *lego.Client) (*registration.Resource, error) {
+	regOption := registration.RegisterOptions{
+		TermsOfServiceAgreed: true,
+	}
+
+	reg, err := client.Registration.Register(regOption)
+	if err != nil {
+		return nil, fmt.Errorf("register failed: %s", err.Error())
+	}
+
+	return reg, nil
+}

+ 92 - 0
src/certssl/applycert/main.go

@@ -0,0 +1,92 @@
+package applycert
+
+import (
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/certssl/account"
+	"github.com/SongZihuan/huan-proxy/src/utils"
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/providers/dns/alidns"
+	"time"
+)
+
+const DefaultCertTimeout = 30 * 24 * time.Hour
+const DefaultCertType = certcrypto.RSA4096
+
+func ApplyCert(basedir string, email string, aliyunAccessKey string, aliyunAccessSecret string, domain string) (*certificate.Resource, error) {
+	if domain == "" || !utils.IsValidDomain(domain) {
+		return nil, fmt.Errorf("domain is invalid")
+	}
+
+	user, err := account.LoadAccount(basedir, email)
+	if err != nil {
+		fmt.Printf("load local account failed, register a new on for %s: %s\n", email, err.Error())
+		user, err = account.NewAccount(basedir, email)
+		if err != nil {
+			return nil, fmt.Errorf("generate new user failed: %s", err.Error())
+		}
+	}
+
+	config := lego.NewConfig(user)
+	config.Certificate.KeyType = DefaultCertType
+	config.Certificate.Timeout = DefaultCertTimeout
+	config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
+	client, err := lego.NewClient(config)
+	if err != nil {
+		return nil, fmt.Errorf("new client failed: %s", err.Error())
+	}
+
+	aliyunDnsConfig := alidns.NewDefaultConfig()
+	aliyunDnsConfig.APIKey = aliyunAccessKey
+	aliyunDnsConfig.SecretKey = aliyunAccessSecret
+
+	provider, err := alidns.NewDNSProviderConfig(aliyunDnsConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to initialize AliDNS provider: %s", err.Error())
+	}
+
+	err = client.Challenge.SetDNS01Provider(provider)
+	if err != nil {
+		return nil, fmt.Errorf("set challenge dns1 provider failed: %s", err.Error())
+	}
+
+	reg, err := user.Register(client)
+	if err != nil {
+		return nil, fmt.Errorf("get account failed: %s", err.Error())
+	} else if reg == nil {
+		return nil, fmt.Errorf("get account failed: return nil account.resurce, unknown reason")
+	}
+
+	request := certificate.ObtainRequest{
+		Domains: []string{domain},
+		Bundle:  true,
+	}
+
+	resource, err := client.Certificate.Obtain(request)
+	if err != nil {
+		return nil, fmt.Errorf("obtain certificate failed: %s", err.Error())
+	}
+
+	err = user.SaveAccount()
+	if err != nil {
+		return nil, fmt.Errorf("save account error after obtain: %s", err.Error())
+	}
+
+	cert, err := utils.ReadCertificate(resource.Certificate)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read certificate: %s", err.Error())
+	}
+
+	err = writerWithDate(basedir, cert, resource)
+	if err != nil {
+		return nil, fmt.Errorf("writer certificate backup failed: %s", err.Error())
+	}
+
+	err = writer(basedir, cert, resource)
+	if err != nil {
+		return nil, fmt.Errorf("writer certificate failed: %s", err.Error())
+	}
+
+	return resource, nil
+}

+ 76 - 0
src/certssl/applycert/read.go

@@ -0,0 +1,76 @@
+package applycert
+
+import (
+	"crypto"
+	"crypto/x509"
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/certssl/filename"
+	"github.com/SongZihuan/huan-proxy/src/utils"
+	"os"
+	"path"
+)
+
+func ReadLocalCertificateAndPrivateKey(basedir string, domain string) (crypto.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
+	dir := path.Join(basedir, domain)
+	cert, err := readCertificate(dir)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read certificate failed: %s", err.Error())
+	}
+
+	cacert, err := readCACertificate(dir)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read certificate failed: %s", err.Error())
+	}
+
+	privateKey, err := readPrivateKey(dir)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read private key failed: %s", err.Error())
+	}
+
+	return privateKey, cert, cacert, nil
+}
+
+func readCertificate(dir string) (*x509.Certificate, error) {
+	filepath := path.Join(dir, filename.FileCertificate)
+	data, err := os.ReadFile(filepath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read certificate file: %v", err)
+	}
+
+	cert, err := utils.ReadCertificate(data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parser certificate file: %v", err)
+	}
+
+	return cert, nil
+}
+
+func readCACertificate(dir string) (*x509.Certificate, error) {
+	filepath := path.Join(dir, filename.FileIssuerCertificate)
+	data, err := os.ReadFile(filepath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read certificate file: %v", err)
+	}
+
+	cert, err := utils.ReadCertificate(data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parser certificate file: %v", err)
+	}
+
+	return cert, nil
+}
+
+func readPrivateKey(dir string) (crypto.PrivateKey, error) {
+	filepath := path.Join(dir, filename.FilePrivateKey)
+	data, err := os.ReadFile(filepath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read key file: %v", err)
+	}
+
+	privateKey, err := utils.ReadPrivateKey(data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parser key file: %v", err)
+	}
+
+	return privateKey, nil
+}

+ 97 - 0
src/certssl/applycert/write.go

@@ -0,0 +1,97 @@
+package applycert
+
+import (
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/certssl/filename"
+	"github.com/go-acme/lego/v4/certificate"
+	"os"
+	"path"
+)
+
+func writerWithDate(basedir string, cert *x509.Certificate, resource *certificate.Resource) error {
+	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())
+
+	backupdir := path.Join(basedir, "cert-backup", domain, year, month, day, cert.NotBefore.Format("2006-01-02-15:04:05"))
+	err := os.MkdirAll(backupdir, 0775)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(backupdir, filename.FilePrivateKey), resource.PrivateKey, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(backupdir, filename.FileCertificate), resource.Certificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(backupdir, filename.FileIssuerCertificate), resource.IssuerCertificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(backupdir, filename.FileCSR), resource.CSR, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	data, err := json.Marshal(resource)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(backupdir, filename.FileResource), data, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func writer(basedir string, cert *x509.Certificate, resource *certificate.Resource) error {
+	domain := cert.Subject.CommonName
+	if domain == "" && len(cert.DNSNames) == 0 {
+		return fmt.Errorf("no domains in certificate")
+	}
+	domain = cert.DNSNames[0]
+
+	dir := path.Join(basedir, domain)
+	err := os.MkdirAll(dir, 0775)
+	if err != nil {
+		return fmt.Errorf("failed to create directory %s: %s", dir, err.Error())
+	}
+
+	err = os.WriteFile(path.Join(dir, filename.FilePrivateKey), resource.PrivateKey, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, filename.FileCertificate), resource.Certificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, filename.FileIssuerCertificate), resource.IssuerCertificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, filename.FileCSR), resource.CSR, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 9 - 0
src/certssl/filename/filename.go

@@ -0,0 +1,9 @@
+package filename
+
+const (
+	FilePrivateKey        = "private.key"
+	FileCertificate       = "cert.pem"
+	FileIssuerCertificate = "ca-cert.pem"
+	FileCSR               = "csr.pem"
+	FileResource          = "resource.json"
+)

+ 131 - 0
src/certssl/main.go

@@ -0,0 +1,131 @@
+package certssl
+
+import (
+	"crypto"
+	"crypto/x509"
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/certssl/applycert"
+	"github.com/SongZihuan/huan-proxy/src/utils"
+	"time"
+)
+
+const CertDefaultNewApplyTime = 5 * 24 * time.Hour
+
+func GetCertificateAndPrivateKey(basedir string, email string, aliyunAccessKey string, aliyunAccessSecret string, domain string) (crypto.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
+	if email == "" {
+		email = "no-reply@example.com"
+	}
+
+	if !utils.IsValidEmail(email) {
+		return nil, nil, nil, fmt.Errorf("not a valid email")
+	}
+
+	if !utils.IsValidDomain(domain) {
+		return nil, nil, nil, fmt.Errorf("not a valid domain")
+	}
+
+	privateKey, cert, cacert, err := applycert.ReadLocalCertificateAndPrivateKey(basedir, domain)
+	if err == nil && utils.CheckCertWithDomain(cert, domain) && utils.CheckCertWithTime(cert, 5*24*time.Hour) {
+		return privateKey, cert, cacert, nil
+	}
+
+	resource, err := applycert.ApplyCert(basedir, email, aliyunAccessKey, aliyunAccessSecret, domain)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("apply cert failed: %s", err.Error())
+	} else if resource == nil {
+		return nil, nil, nil, fmt.Errorf("read cert failed: private key or certificate (resource) is nil, unknown reason")
+	}
+
+	privateKey, err = utils.ReadPrivateKey(resource.PrivateKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read private key failed: %s", err.Error())
+	}
+
+	cert, err = utils.ReadCertificate(resource.Certificate)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read cert failed: %s", err.Error())
+	}
+
+	cacert, err = utils.ReadCertificate(resource.IssuerCertificate)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read cert failed: %s", err.Error())
+	}
+
+	return privateKey, cert, cacert, nil
+}
+
+type NewCert struct {
+	PrivateKey        crypto.PrivateKey
+	Certificate       *x509.Certificate
+	IssuerCertificate *x509.Certificate
+	Error             error
+}
+
+func WatchCertificate(dir string, email string, aliyunAccessKey string, aliyunAccessSecret string, domain string, 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, cacert, err := watchCertificate(dir, email, aliyunAccessKey, aliyunAccessSecret, domain, oldCert)
+			if err != nil {
+				newchan <- NewCert{
+					Error: fmt.Errorf("watch cert failed: %s", err.Error()),
+				}
+			} else if privateKey != nil && cert != nil && cacert != nil {
+				oldCert = cert
+				newchan <- NewCert{
+					PrivateKey:        privateKey,
+					Certificate:       cert,
+					IssuerCertificate: cacert,
+				}
+			}
+		}
+	}
+}
+
+func watchCertificate(dir string, email string, aliyunAccessKey string, aliyunAccessSecret string, domain string, oldCert *x509.Certificate) (crypto.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
+	if email == "" {
+		email = "no-reply@example.com"
+	}
+
+	if !utils.IsValidEmail(email) {
+		return nil, nil, nil, fmt.Errorf("not a valid email")
+	}
+
+	if !utils.IsValidDomain(domain) {
+		return nil, nil, nil, fmt.Errorf("not a valid domain")
+	}
+
+	if utils.CheckCertWithDomain(oldCert, domain) && utils.CheckCertWithTime(oldCert, CertDefaultNewApplyTime) {
+		return nil, nil, nil, nil
+	}
+
+	resource, err := applycert.ApplyCert(dir, email, aliyunAccessKey, aliyunAccessSecret, domain)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("apply cert fail: %s", err.Error())
+	}
+
+	privateKey, err := utils.ReadPrivateKey(resource.PrivateKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read private key failed: %s", err.Error())
+	}
+
+	cert, err := utils.ReadCertificate(resource.Certificate)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read cert failed: %s", err.Error())
+	}
+
+	cacert, err := utils.ReadCertificate(resource.IssuerCertificate)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read cert failed: %s", err.Error())
+	}
+
+	return privateKey, cert, cacert, nil
+}

+ 5 - 1
src/config/httpconfig.go

@@ -13,7 +13,7 @@ type HttpConfig struct {
 
 func (h *HttpConfig) SetDefault() {
 	if h.Address == "" {
-		h.Address = "localhost:2689"
+		return
 	}
 
 	if h.StopWaitSecond <= 0 {
@@ -22,6 +22,10 @@ func (h *HttpConfig) SetDefault() {
 }
 
 func (h *HttpConfig) Check() configerr.ConfigError {
+	if h.Address == "" {
+		return nil
+	}
+
 	if _, err := url.Parse(h.Address); err != nil {
 		return configerr.NewConfigError(fmt.Sprintf("http address error: %s", err.Error()))
 	}

+ 69 - 0
src/config/httpsconfig.go

@@ -0,0 +1,69 @@
+package config
+
+import (
+	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/config/configerr"
+	"net/url"
+	"os"
+)
+
+const (
+	EnvAliyunKey    = "HP_ALIYUN_ACCESS_KEY"
+	EnvAliyunSecret = "HP_ALIYUN_ACCESS_SECRET"
+)
+
+type HttpsConfig struct {
+	Address               string `yaml:"address"`
+	SSLEmail              string `json:"sslemail"`
+	SSLDomain             string `yaml:"ssldomaain"`
+	SSLCertDir            string `yaml:"sslcertdir"`
+	AliyunDNSAccessKey    string `yaml:"aliyundnsaccesskey"`
+	AliyunDNSAccessSecret string `yaml:"aliyunDNSAccesssecret"`
+	StopWaitSecond        int    `yaml:"stopwaitsecond"`
+}
+
+func (h *HttpsConfig) SetDefault() {
+	if h.Address == "" {
+		return
+	}
+
+	if h.SSLEmail == "" {
+		h.SSLEmail = "no-reply@example.com"
+	}
+
+	if h.SSLCertDir == "" {
+		h.SSLCertDir = "./ssl-certs"
+	}
+
+	if h.AliyunDNSAccessKey == "" {
+		h.AliyunDNSAccessKey = os.Getenv(EnvAliyunKey)
+	}
+
+	if h.AliyunDNSAccessSecret == "" {
+		h.AliyunDNSAccessKey = os.Getenv(EnvAliyunSecret)
+	}
+
+	if h.StopWaitSecond <= 0 {
+		h.StopWaitSecond = 10
+	}
+}
+
+func (h *HttpsConfig) Check() configerr.ConfigError {
+	if h.Address == "" {
+		return nil
+	}
+
+	if _, err := url.Parse(h.Address); err != nil {
+		return configerr.NewConfigError(fmt.Sprintf("http address error: %s", err.Error()))
+	}
+
+	if h.SSLDomain == "" {
+		return configerr.NewConfigError("http ssl must has a domain")
+	}
+
+	if h.AliyunDNSAccessKey == "" || h.AliyunDNSAccessSecret == "" {
+		return configerr.NewConfigError("http ssl must has a aliyun access key or secret")
+	}
+
+	return nil
+}

+ 2 - 1
src/config/yamlconfig.go

@@ -9,7 +9,8 @@ import (
 
 type YamlConfig struct {
 	GlobalConfig         `yaml:",inline"`
-	Http                 HttpConfig `yaml:"http"`
+	Http                 HttpConfig  `yaml:"http"`
+	Https                HttpsConfig `yaml:"https"`
 	rules.RuleListConfig `yaml:",inline"`
 }
 

+ 15 - 4
src/mainfunc/v1.go

@@ -2,6 +2,7 @@ package mainfunc
 
 import (
 	"errors"
+	"fmt"
 	"github.com/SongZihuan/huan-proxy/src/config"
 	"github.com/SongZihuan/huan-proxy/src/config/configwatcher"
 	"github.com/SongZihuan/huan-proxy/src/flagparser"
@@ -63,13 +64,15 @@ func MainV1() int {
 	logger.Executablef("%s", "ready")
 	logger.Infof("run mode: %s", config.GetConfig().GlobalConfig.GetRunMode())
 
-	ser := server.NewServer()
+	ser := server.NewHuanProxyServer()
 
+	httpschan := make(chan error)
 	httpchan := make(chan error)
 
-	go func() {
-		httpchan <- ser.RunHttp()
-	}()
+	err = ser.Run(httpschan, httpchan)
+	if err != nil {
+		return utils.ExitByErrorMsg(fmt.Sprintf("run http/https error: %s", err.Error()))
+	}
 
 	select {
 	case <-config.GetSignalChan():
@@ -82,6 +85,14 @@ func MainV1() int {
 		} else {
 			return 0
 		}
+	case err := <-httpschan:
+		if errors.Is(err, server.ServerStop) {
+			return 0
+		} else if err != nil {
+			return utils.ExitByError(err)
+		} else {
+			return 0
+		}
 	}
 
 	return 0

+ 5 - 5
src/server/api.go

@@ -10,7 +10,7 @@ import (
 	"strings"
 )
 
-func (s *HTTPServer) apiServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) apiServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
 	proxy := rule.Api.Server
 	if proxy == nil {
 		s.abortServerError(w)
@@ -57,7 +57,7 @@ func (s *HTTPServer) apiServer(rule *rulescompile.RuleCompileConfig, w http.Resp
 	proxy.ServeHTTP(w, r) // 反向代理
 }
 
-func (s *HTTPServer) apiRewrite(srcpath string, prefix string, suffix string, rewrite *rewritecompile.RewriteCompileConfig) string {
+func (s *HuanProxyServer) apiRewrite(srcpath string, prefix string, suffix string, rewrite *rewritecompile.RewriteCompileConfig) string {
 	srcpath = utils.ProcessURLPath(srcpath)
 	prefix = utils.ProcessURLPath(prefix)
 	suffix = utils.ProcessURLPath(suffix)
@@ -75,7 +75,7 @@ func (s *HTTPServer) apiRewrite(srcpath string, prefix string, suffix string, re
 	return srcpath
 }
 
-func (s *HTTPServer) processProxyHeader(r *http.Request) {
+func (s *HuanProxyServer) processProxyHeader(r *http.Request) {
 	if r.RemoteAddr == "" {
 		return
 	}
@@ -124,7 +124,7 @@ func (s *HTTPServer) processProxyHeader(r *http.Request) {
 	r.Header.Set("X-Forwarded-Proto", proto)
 }
 
-func (s *HTTPServer) getProxyListForwarder(remoteIP net.IP, r *http.Request) ([]string, []string, string, string) {
+func (s *HuanProxyServer) getProxyListForwarder(remoteIP net.IP, r *http.Request) ([]string, []string, string, string) {
 	ForwardedList := strings.Split(r.Header.Get("Forwarded"), ",")
 	ProxyList := make([]string, 0, len(ForwardedList)+1)
 	NewForwardedList := make([]string, 0, len(ForwardedList)+1)
@@ -168,7 +168,7 @@ func (s *HTTPServer) getProxyListForwarder(remoteIP net.IP, r *http.Request) ([]
 	return ProxyList, NewForwardedList, host, proto
 }
 
-func (s *HTTPServer) getProxyListFromXForwardedFor(remoteIP net.IP, r *http.Request) ([]string, []string, string, string) {
+func (s *HuanProxyServer) getProxyListFromXForwardedFor(remoteIP net.IP, r *http.Request) ([]string, []string, string, string) {
 	XFroWardedForList := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
 	ProxyList := make([]string, 0, len(XFroWardedForList)+1)
 	NewForwardedList := make([]string, 0, len(XFroWardedForList)+1)

+ 1 - 1
src/server/cors.go

@@ -6,7 +6,7 @@ import (
 	"net/http"
 )
 
-func (s *HTTPServer) cors(corsRule *corscompile.CorsCompileConfig, w http.ResponseWriter, r *http.Request) bool {
+func (s *HuanProxyServer) cors(corsRule *corscompile.CorsCompileConfig, w http.ResponseWriter, r *http.Request) bool {
 	if corsRule.Ignore {
 		if r.Method == http.MethodOptions {
 			s.abortMethodNotAllowed(w)

+ 4 - 4
src/server/dir.go

@@ -15,7 +15,7 @@ import (
 const IndexMaxDeep = 5
 const DefaultIgnoreFileMap = 20
 
-func (s *HTTPServer) dirServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) dirServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
 	if !s.cors(rule.Dir.Cors, w, r) {
 		return
 	}
@@ -94,7 +94,7 @@ func (s *HTTPServer) dirServer(rule *rulescompile.RuleCompileConfig, w http.Resp
 	s.statusOK(w)
 }
 
-func (s *HTTPServer) dirRewrite(srcpath string, prefix string, suffix string, rewrite *rewritecompile.RewriteCompileConfig) string {
+func (s *HuanProxyServer) dirRewrite(srcpath string, prefix string, suffix string, rewrite *rewritecompile.RewriteCompileConfig) string {
 	if strings.HasPrefix(srcpath, suffix) {
 		srcpath = srcpath[len(suffix):]
 	}
@@ -108,11 +108,11 @@ func (s *HTTPServer) dirRewrite(srcpath string, prefix string, suffix string, re
 	return srcpath
 }
 
-func (s *HTTPServer) getIndexFile(rule *rulescompile.RuleCompileConfig, dir string) string {
+func (s *HuanProxyServer) getIndexFile(rule *rulescompile.RuleCompileConfig, dir string) string {
 	return s._getIndexFile(rule, dir, "", IndexMaxDeep)
 }
 
-func (s *HTTPServer) _getIndexFile(rule *rulescompile.RuleCompileConfig, baseDir string, nextDir string, deep int) string {
+func (s *HuanProxyServer) _getIndexFile(rule *rulescompile.RuleCompileConfig, baseDir string, nextDir string, deep int) string {
 	if deep == 0 {
 		return ""
 	}

+ 1 - 1
src/server/file.go

@@ -8,7 +8,7 @@ import (
 	"os"
 )
 
-func (s *HTTPServer) fileServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) fileServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
 	if !s.cors(rule.File.Cors, w, r) {
 		return
 	}

+ 2 - 2
src/server/loggerserver.go

@@ -110,7 +110,7 @@ func (p *LogFormatterParams) ResetColor() string {
 	return reset
 }
 
-func (s *HTTPServer) Formatter(param LogFormatterParams) string {
+func (s *HuanProxyServer) Formatter(param LogFormatterParams) string {
 	var statusColor, methodColor, resetColor string
 	if s.isTerm {
 		statusColor = param.StatusCodeColor()
@@ -131,7 +131,7 @@ func (s *HTTPServer) Formatter(param LogFormatterParams) string {
 	)
 }
 
-func (s *HTTPServer) LoggerServerHTTP(_w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+func (s *HuanProxyServer) LoggerServerHTTP(_w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
 	// Start timer
 	start := time.Now()
 	path := r.URL.Path

+ 1 - 1
src/server/match.go

@@ -8,7 +8,7 @@ import (
 	"strings"
 )
 
-func (s *HTTPServer) matchURL(rule *rulescompile.RuleCompileConfig, r *http.Request) bool {
+func (s *HuanProxyServer) matchURL(rule *rulescompile.RuleCompileConfig, r *http.Request) bool {
 	url := utils.ProcessURLPath(r.URL.Path)
 	if rule.MatchType == matchcompile.RegexMatch {
 		if rule.MatchRegex.MatchString(url) || rule.MatchRegex.MatchString(url+"/") {

+ 1 - 1
src/server/proxytrust.go

@@ -7,7 +7,7 @@ import (
 	"net/http"
 )
 
-func (s *HTTPServer) checkProxyTrust(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) bool {
+func (s *HuanProxyServer) checkProxyTrust(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) bool {
 	if !rule.UseTrustedIPs {
 		return true
 	}

+ 2 - 2
src/server/redirect.go

@@ -8,7 +8,7 @@ import (
 	"net/url"
 )
 
-func (s *HTTPServer) redirectServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) redirectServer(rule *rulescompile.RuleCompileConfig, w http.ResponseWriter, r *http.Request) {
 	target := s.redirectRewrite(rule.Redirect.Address, rule.Redirect.Rewrite)
 
 	if _, err := url.Parse(target); err != nil {
@@ -20,7 +20,7 @@ func (s *HTTPServer) redirectServer(rule *rulescompile.RuleCompileConfig, w http
 	s.statusRedirect(w, r, target, rule.Redirect.Code)
 }
 
-func (s *HTTPServer) redirectRewrite(address string, rewrite *rewritecompile.RewriteCompileConfig) string {
+func (s *HuanProxyServer) redirectRewrite(address string, rewrite *rewritecompile.RewriteCompileConfig) string {
 	if rewrite.Use && rewrite.Regex != nil {
 		rewrite.Regex.ReplaceAllString(address, rewrite.Target)
 	}

+ 10 - 10
src/server/respose.go

@@ -13,7 +13,7 @@ import (
 const XHuanProxyHeaer = apicompile.XHuanProxyHeaer
 const ViaHeader = apicompile.ViaHeader
 
-func (s *HTTPServer) writeHuanProxyHeader(r *http.Request) {
+func (s *HuanProxyServer) writeHuanProxyHeader(r *http.Request) {
 	version := strings.TrimSpace(utils.StringToOnlyPrint(resource.Version))
 	h := r.Header.Get(XHuanProxyHeaer)
 	if h == "" {
@@ -25,7 +25,7 @@ func (s *HTTPServer) writeHuanProxyHeader(r *http.Request) {
 	r.Header.Set(XHuanProxyHeaer, h)
 }
 
-func (s *HTTPServer) writeViaHeader(rule *rulescompile.RuleCompileConfig, r *http.Request) {
+func (s *HuanProxyServer) writeViaHeader(rule *rulescompile.RuleCompileConfig, r *http.Request) {
 	info := fmt.Sprintf("%s %s", r.Proto, rule.Api.Via)
 
 	h := r.Header.Get(ViaHeader)
@@ -38,34 +38,34 @@ func (s *HTTPServer) writeViaHeader(rule *rulescompile.RuleCompileConfig, r *htt
 	r.Header.Set(ViaHeader, h)
 }
 
-func (s *HTTPServer) abortForbidden(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortForbidden(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusForbidden)
 }
 
-func (s *HTTPServer) abortNotFound(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortNotFound(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusNotFound)
 }
 
-func (s *HTTPServer) abortNotAcceptable(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortNotAcceptable(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusNotAcceptable)
 }
 
-func (s *HTTPServer) abortMethodNotAllowed(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortMethodNotAllowed(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusMethodNotAllowed)
 }
 
-func (s *HTTPServer) abortServerError(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortServerError(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusInternalServerError)
 }
 
-func (s *HTTPServer) abortNoContent(w http.ResponseWriter) {
+func (s *HuanProxyServer) abortNoContent(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusNoContent)
 }
 
-func (s *HTTPServer) statusOK(w http.ResponseWriter) {
+func (s *HuanProxyServer) statusOK(w http.ResponseWriter) {
 	w.WriteHeader(http.StatusOK)
 }
 
-func (s *HTTPServer) statusRedirect(w http.ResponseWriter, r *http.Request, url string, code int) {
+func (s *HuanProxyServer) statusRedirect(w http.ResponseWriter, r *http.Request, url string, code int) {
 	http.Redirect(w, r, url, code)
 }

+ 223 - 23
src/server/server.go

@@ -1,70 +1,270 @@
 package server
 
 import (
+	"context"
+	"crypto"
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
+	"github.com/SongZihuan/huan-proxy/src/certssl"
 	"github.com/SongZihuan/huan-proxy/src/config"
 	"github.com/SongZihuan/huan-proxy/src/config/rulescompile"
 	"github.com/SongZihuan/huan-proxy/src/flagparser"
 	"github.com/SongZihuan/huan-proxy/src/logger"
 	"net/http"
+	"sync"
+	"time"
 )
 
 var ServerStop = fmt.Errorf("server stop")
 
 type HTTPServer struct {
-	address string
-	skip    map[string]struct{}
-	isTerm  bool
-	writer  func(msg string)
+	cfg     *config.HttpConfig
+	server  *http.Server
+	handler http.Handler
 }
 
-func NewServer() *HTTPServer {
+type HTTPSServer struct {
+	cfg         *config.HttpsConfig
+	reloadMutex sync.Mutex
+	key         crypto.PrivateKey
+	cert        *x509.Certificate
+	cacert      *x509.Certificate
+	server      *http.Server
+	handler     http.Handler
+}
+
+type LogServer struct {
+	skip   map[string]struct{}
+	isTerm bool
+	writer func(msg string)
+}
+
+type HuanProxyServer struct {
+	http  HTTPServer
+	https HTTPSServer
+	LogServer
+}
+
+func NewHuanProxyServer() *HuanProxyServer {
 	if !flagparser.IsReady() || !config.IsReady() {
 		panic("not ready")
 	}
 
-	var skip = make(map[string]struct{}, 10)
+	skip := make(map[string]struct{}, 10)
+	httpcfg := config.GetConfig().Http
+	httpscfg := config.GetConfig().Https
+
+	res := &HuanProxyServer{
+		http: HTTPServer{
+			cfg:    &httpcfg,
+			server: nil,
+		},
 
-	return &HTTPServer{
-		address: config.GetConfig().Http.Address,
-		skip:    skip,
-		isTerm:  logger.IsInfoTermNotDumb(),
-		writer:  logger.InfoWrite,
+		https: HTTPSServer{
+			cfg:    &httpscfg,
+			server: nil,
+		},
+
+		LogServer: LogServer{
+			skip:   skip,
+			isTerm: logger.IsInfoTermNotDumb(),
+			writer: logger.InfoWrite,
+		},
 	}
+
+	res.http.handler = res
+	res.https.handler = res
+
+	return res
 }
 
-func (s *HTTPServer) GetConfig() *config.YamlConfig {
+func (s *HuanProxyServer) GetConfig() *config.YamlConfig {
 	// 不用检查Ready,因为在NewServer的时候已经检查过了
 	return config.GetConfig()
 }
 
-func (s *HTTPServer) GetRules() *rulescompile.RuleListCompileConfig {
+func (s *HuanProxyServer) GetRules() *rulescompile.RuleListCompileConfig {
 	// 不用检查Ready,因为在NewServer的时候已经检查过了
 	return config.GetRules()
 }
 
-func (s *HTTPServer) GetRulesList() []*rulescompile.RuleCompileConfig {
+func (s *HuanProxyServer) GetRulesList() []*rulescompile.RuleCompileConfig {
 	// 不用检查Ready,因为在NewServer的时候已经检查过了
 	return s.GetRules().Rules
 }
 
-func (s *HTTPServer) RunHttp() error {
-	err := s.runHttp()
-	if errors.Is(err, http.ErrServerClosed) {
-		return ServerStop
-	} else if err != nil {
+func (s *HuanProxyServer) Run(httpschan chan error, httpchan chan error) (err error) {
+	if s.https.cfg.Address != "" {
+		err := s.https.loadHttps()
+		if err != nil {
+			return err
+		}
+
+		s.https.runHttps(httpschan)
+	}
+
+	if s.http.cfg.Address != "" {
+		err := s.http.loadHttp()
+		if err != nil {
+			return err
+		}
+
+		s.http.runHttp(httpchan)
+	}
+
+	return nil
+}
+
+func (s *HTTPSServer) loadHttps() error {
+	privateKey, certificate, issuerCertificate, err := certssl.GetCertificateAndPrivateKey(s.cfg.SSLCertDir, s.cfg.SSLEmail, s.cfg.AliyunDNSAccessKey, s.cfg.AliyunDNSAccessSecret, s.cfg.SSLDomain)
+	if err != nil {
+		return fmt.Errorf("init htttps cert ssl server error: %s", err.Error())
+	} else if privateKey == nil || certificate == nil || issuerCertificate == nil {
+		return fmt.Errorf("init https server error: get key and cert error, return nil, unknown reason")
+	}
+
+	s.key = privateKey
+	s.cert = certificate
+	s.cacert = issuerCertificate
+
+	err = s.reloadHttps()
+	if err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func (s *HTTPServer) runHttp() error {
-	logger.Infof("start server in %s", s.address)
-	return http.ListenAndServe(s.address, s)
+func (s *HTTPSServer) reloadHttps() error {
+	if s.key == nil || s.cert == nil || s.cacert == nil {
+		return fmt.Errorf("init https server error: get key and cert error, return nil, unknown reason")
+	}
+
+	if s.cert.Raw == nil || len(s.cert.Raw) == 0 || s.cacert.Raw == nil || len(s.cacert.Raw) == 0 {
+		return fmt.Errorf("init https server error: get cert.raw error, return nil, unknown reason")
+	}
+
+	tlsConfig := &tls.Config{
+		Certificates: []tls.Certificate{{
+			Certificate: [][]byte{s.cert.Raw, s.cacert.Raw}, // Raw包含 DER 编码的证书
+			PrivateKey:  s.key,
+			Leaf:        s.cert,
+		}},
+		MinVersion: tls.VersionTLS12,
+	}
+
+	s.server = &http.Server{
+		Addr:      s.cfg.Address,
+		Handler:   s.handler,
+		TLSConfig: tlsConfig,
+	}
+
+	return nil
+}
+
+func (s *HTTPSServer) runHttps(_httpschan chan error) chan error {
+	_watchstopchan := make(chan bool)
+
+	s.watchCertificate(_watchstopchan)
+
+	go func(httpschan chan error, watchstopchan chan bool) {
+		defer func() {
+			watchstopchan <- true
+		}()
+	ListenCycle:
+		for {
+			logger.Infof("start server in %s", s.cfg.Address)
+			err := s.server.ListenAndServeTLS("", "")
+			if err != nil && errors.Is(err, http.ErrServerClosed) {
+				if s.reloadMutex.TryLock() {
+					s.reloadMutex.Unlock()
+					_httpschan <- ServerStop
+					return
+				}
+				s.reloadMutex.Lock()
+				s.reloadMutex.Unlock() // 等待证书更换完毕
+				continue ListenCycle
+			} else if err != nil {
+				_httpschan <- fmt.Errorf("https server error: %s", err.Error())
+				return
+			}
+		}
+	}(_httpschan, _watchstopchan)
+
+	return _httpschan
+}
+
+func (s *HTTPSServer) watchCertificate(stopchan chan bool) {
+	newchan := make(chan certssl.NewCert)
+
+	go func() {
+		err := certssl.WatchCertificate(s.cfg.SSLCertDir, s.cfg.SSLEmail, s.cfg.AliyunDNSAccessKey, s.cfg.AliyunDNSAccessSecret, s.cfg.SSLDomain, s.cert, stopchan, newchan)
+		if err != nil {
+			fmt.Printf("watch https cert server 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("https cert reload server error: %s", res.Error.Error())
+			} else if res.PrivateKey != nil && res.Certificate != nil && res.IssuerCertificate != nil {
+				func() {
+					s.reloadMutex.Lock()
+					defer s.reloadMutex.Unlock()
+
+					ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+					defer cancel()
+
+					err := s.server.Shutdown(ctx)
+					if err != nil {
+						fmt.Printf("https server reload shutdown error: %s", err.Error())
+					}
+
+					s.key = res.PrivateKey
+					s.cert = res.Certificate
+					s.cacert = res.IssuerCertificate
+
+					err = s.reloadHttps()
+					if err != nil {
+						fmt.Printf("https server reload init error: %s", err.Error())
+					}
+				}()
+			}
+		}
+	}()
+}
+
+func (s *HTTPServer) loadHttp() error {
+	s.server = &http.Server{
+		Addr:    s.cfg.Address,
+		Handler: s.handler,
+	}
+	return nil
+}
+
+func (s *HTTPServer) runHttp(_httpschan chan error) chan error {
+	go func(httpschan chan error) {
+		logger.Infof("start server in %s", s.cfg.Address)
+		err := s.server.ListenAndServe()
+		if err != nil && errors.Is(err, http.ErrServerClosed) {
+			httpschan <- ServerStop
+			return
+		} else if err != nil {
+			httpschan <- err
+			return
+		}
+	}(_httpschan)
+
+	return _httpschan
 }
 
-func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	s.LoggerServerHTTP(w, r, s.NormalServeHTTP)
 }

+ 1 - 1
src/server/serverhttp.go

@@ -6,7 +6,7 @@ import (
 	"net/http"
 )
 
-func (s *HTTPServer) NormalServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (s *HuanProxyServer) NormalServeHTTP(w http.ResponseWriter, r *http.Request) {
 	s.writeHuanProxyHeader(r)
 
 	func() {

+ 67 - 0
src/utils/privatekey.go

@@ -0,0 +1,67 @@
+package utils
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+)
+
+func ReadPrivateKey(data []byte) (crypto.PrivateKey, error) {
+	// 解析PEM块
+	block, _ := pem.Decode(data)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode PEM block containing private key")
+	}
+
+	// 根据PEM块类型解析私钥
+	var err error
+	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: %s", err.Error())
+	} else if privateKey == nil {
+		return nil, fmt.Errorf("failed to parse private ket: return nil, unknown reason")
+	}
+
+	return privateKey, nil
+}
+
+func EncodePrivateKeyToPEM(privateKey crypto.PrivateKey) ([]byte, error) {
+	var pemType string
+	var bytes []byte
+	var err error
+
+	switch priv := privateKey.(type) {
+	case *rsa.PrivateKey:
+		pemType = "RSA PRIVATE KEY"
+		bytes = x509.MarshalPKCS1PrivateKey(priv)
+	case *ecdsa.PrivateKey:
+		pemType = "EC PRIVATE KEY"
+		bytes, err = x509.MarshalECPrivateKey(priv)
+	default:
+		return nil, fmt.Errorf("unsupported private key type")
+	}
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal private key: %s", err.Error())
+	}
+
+	block := &pem.Block{
+		Type:  pemType,
+		Bytes: bytes,
+	}
+
+	return pem.EncodeToMemory(block), nil
+}

+ 54 - 0
src/utils/x509.go

@@ -0,0 +1,54 @@
+package utils
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"time"
+)
+
+func ReadCertificate(data []byte) (*x509.Certificate, error) {
+	block, _ := pem.Decode(data)
+	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: %s", err.Error())
+	} else if cert == nil {
+		return nil, fmt.Errorf("failed to parse certificate: return nil, unknown reason")
+	}
+
+	return 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.NotAfter) {
+		return false
+	}
+
+	return true
+}