Skip to content

Commit

Permalink
feat: 证书
Browse files Browse the repository at this point in the history
  • Loading branch information
devhaozi committed Sep 16, 2024
1 parent b5509ec commit c781ab0
Show file tree
Hide file tree
Showing 8 changed files with 691 additions and 13 deletions.
20 changes: 19 additions & 1 deletion internal/biz/cert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package biz

import "github.com/golang-module/carbon/v2"
import (
"github.com/golang-module/carbon/v2"

"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
)

type Cert struct {
ID uint `gorm:"primaryKey" json:"id"`
Expand All @@ -20,3 +25,16 @@ type Cert struct {
User *CertUser `gorm:"foreignKey:UserID" json:"user"`
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
}

type CertRepo interface {
List(page, limit uint) ([]*Cert, int64, error)
Get(id uint) (*Cert, error)
Create(req *request.CertCreate) (*Cert, error)
Update(req *request.CertUpdate) error
Delete(id uint) error
ObtainAuto(id uint) (*acme.Certificate, error)
ObtainManual(id uint) (*acme.Certificate, error)
Renew(id uint) (*acme.Certificate, error)
ManualDNS(id uint) ([]acme.DNSRecord, error)
Deploy(ID, WebsiteID uint) error
}
5 changes: 4 additions & 1 deletion internal/biz/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ type Task struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;index" json:"name"`
Status string `gorm:"not null;default:'waiting'" json:"status"`
Shell string `gorm:"not null" json:"shell"`
Shell string `gorm:"not null" json:"-"`
Log string `gorm:"not null" json:"log"`
CreatedAt carbon.DateTime `json:"created_at"`
UpdatedAt carbon.DateTime `json:"updated_at"`
}

type TaskRepo interface {
HasRunningTask() bool
List(page, limit uint) ([]*Task, int64, error)
Get(id uint) (*Task, error)
Delete(id uint) error
}
273 changes: 273 additions & 0 deletions internal/data/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package data

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)

type certRepo struct {
client *acme.Client
websiteRepo biz.WebsiteRepo
}

func NewCertRepo() biz.CertRepo {
return &certRepo{
websiteRepo: NewWebsiteRepo(),
}
}

func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
var certs []*biz.Cert
var total int64
err := app.Orm.Model(&biz.Cert{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
return certs, total, err
}

func (r *certRepo) Get(id uint) (*biz.Cert, error) {
cert := new(biz.Cert)
err := app.Orm.Model(&biz.Cert{}).Where("id = ?", id).First(cert).Error
return cert, err
}

func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
cert := &biz.Cert{
UserID: req.UserID,
WebsiteID: req.WebsiteID,
DNSID: req.DNSID,
Type: req.Type,
Domains: req.Domains,
AutoRenew: req.AutoRenew,
}
if err := app.Orm.Create(cert).Error; err != nil {
return nil, err
}
return cert, nil
}

func (r *certRepo) Update(req *request.CertUpdate) error {
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Updates(&biz.Cert{
UserID: req.UserID,
WebsiteID: req.WebsiteID,
DNSID: req.DNSID,
Type: req.Type,
Domains: req.Domains,
AutoRenew: req.AutoRenew,
}).Error
}

func (r *certRepo) Delete(id uint) error {
return app.Orm.Model(&biz.Cert{}).Where("id = ?", id).Delete(&biz.Cert{}).Error
}

func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
cert, err := r.Get(id)
if err != nil {
return nil, err
}

client, err := r.getClient(cert)
if err != nil {
return nil, err
}

if cert.DNS != nil {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("该证书没有关联网站,无法自动签发")
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("通配符域名无法使用 HTTP 验证")
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
}
}

ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}

cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
if err = app.Orm.Save(cert).Error; err != nil {
return nil, err
}

if cert.Website != nil {
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
}

return &ssl, nil
}

func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
cert, err := r.Get(id)
if err != nil {
return nil, err
}

if r.client == nil {
return nil, errors.New("请重新获取 DNS 解析记录")
}

ssl, err := r.client.ObtainSSLManual()
if err != nil {
return nil, err
}

cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
if err = app.Orm.Save(cert).Error; err != nil {
return nil, err
}

if cert.Website != nil {
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
}

return &ssl, nil
}

func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
cert, err := r.Get(id)
if err != nil {
return nil, err
}

client, err := r.getClient(cert)
if err != nil {
return nil, err
}

if cert.CertURL == "" {
return nil, errors.New("该证书没有签发成功,无法续签")
}

if cert.DNS != nil {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发")
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("通配符域名无法使用 HTTP 验证")
}
}
conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
}
}

ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}

cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
if err = app.Orm.Save(cert).Error; err != nil {
return nil, err
}

if cert.Website != nil {
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
}

return &ssl, nil
}

func (r *certRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) {
cert, err := r.Get(id)
if err != nil {
return nil, err
}

client, err := r.getClient(cert)
if err != nil {
return nil, err
}

client.UseManualDns(len(cert.Domains))
records, err := client.GetDNSRecords(context.Background(), cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}

// 15 分钟后清理客户端
r.client = client
time.AfterFunc(15*time.Minute, func() {
r.client = nil
})

return records, nil
}

func (r *certRepo) Deploy(ID, WebsiteID uint) error {
cert, err := r.Get(ID)
if err != nil {
return err
}

if cert.Cert == "" || cert.Key == "" {
return errors.New("该证书没有签发成功,无法部署")
}

website, err := r.websiteRepo.Get(WebsiteID)
if err != nil {
return err
}

if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
return err
}
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
return err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return err
}

return nil
}

func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
var ca string
var eab *acme.EAB
switch cert.User.CA {
case "letsencrypt":
ca = acme.CALetsEncrypt
case "buypass":
ca = acme.CABuypass
case "zerossl":
ca = acme.CAZeroSSL
eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded}
case "sslcom":
ca = acme.CASSLcom
eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded}
case "google":
ca = acme.CAGoogle
eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded}
}

return acme.NewPrivateKeyAccount(cert.User.Email, cert.User.PrivateKey, ca, eab)
}
17 changes: 17 additions & 0 deletions internal/data/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,20 @@ func (r *taskRepo) HasRunningTask() bool {
app.Orm.Model(&biz.Task{}).Where("status = ?", biz.TaskStatusRunning).Or("status = ?", biz.TaskStatusWaiting).Count(&count)
return count > 0
}

func (r *taskRepo) List(page, limit uint) ([]*biz.Task, int64, error) {
var tasks []*biz.Task
var total int64
err := app.Orm.Model(&biz.Task{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&tasks).Error
return tasks, total, err
}

func (r *taskRepo) Get(id uint) (*biz.Task, error) {
task := new(biz.Task)
err := app.Orm.Model(&biz.Task{}).Where("id = ?", id).First(task).Error
return task, err
}

func (r *taskRepo) Delete(id uint) error {
return app.Orm.Model(&biz.Task{}).Where("id = ?", id).Delete(&biz.Task{}).Error
}
25 changes: 25 additions & 0 deletions internal/http/request/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package request

type CertCreate struct {
Type string `form:"type" json:"type"`
Domains []string `form:"domains" json:"domains"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
UserID uint `form:"user_id" json:"user_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}

type CertUpdate struct {
ID uint `form:"id" json:"id"`
Type string `form:"type" json:"type"`
Domains []string `form:"domains" json:"domains"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
UserID uint `form:"user_id" json:"user_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}

type CertDeploy struct {
ID uint `form:"id" json:"id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}
5 changes: 3 additions & 2 deletions internal/route/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ func Http(r chi.Router) {
r.Use(middleware.MustLogin)
task := service.NewTaskService()
r.Get("/status", task.Status)
r.Get("/", task.Index)
r.Get("/{id}", task.Show) // TODO 修改前端
r.Get("/", task.List)
r.Get("/{id}", task.Get) // TODO 修改前端
r.Delete("/{id}", task.Delete) // TODO 修改前端
})

Expand All @@ -63,6 +63,7 @@ func Http(r chi.Router) {
r.Post("/{id}/status", website.UpdateStatus)
})

// TODO
r.Route("/backup", func(r chi.Router) {
r.Use(middleware.MustLogin)
backup := service.NewBackupService()
Expand Down
Loading

0 comments on commit c781ab0

Please sign in to comment.