You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1Panel/agent/app/service/website_ca.go

447 lines
13 KiB

package service
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"log"
"math/big"
"net"
"os"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
"github.com/go-acme/lego/v4/certcrypto"
)
type WebsiteCAService struct {
}
type IWebsiteCAService interface {
Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error)
Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
GetCA(id uint) (*response.WebsiteCADTO, error)
Delete(id uint) error
ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error)
DownloadFile(id uint) (*os.File, error)
}
func NewIWebsiteCAService() IWebsiteCAService {
return &WebsiteCAService{}
}
func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) {
total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc"))
if err != nil {
return 0, nil, err
}
var caDTOs []response.WebsiteCADTO
for _, ca := range cas {
caDTOs = append(caDTOs, response.WebsiteCADTO{
WebsiteCA: ca,
})
}
return total, caDTOs, err
}
func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) {
if exist, _ := websiteCARepo.GetFirst(commonRepo.WithByName(create.Name)); exist.ID > 0 {
return nil, buserr.New(constant.ErrNameIsExist)
}
ca := &model.WebsiteCA{
Name: create.Name,
KeyType: create.KeyType,
}
pkixName := pkix.Name{
CommonName: create.CommonName,
Country: []string{create.Country},
Organization: []string{create.Organization},
OrganizationalUnit: []string{create.OrganizationUint},
}
if create.Province != "" {
pkixName.Province = []string{create.Province}
}
if create.City != "" {
pkixName.Locality = []string{create.City}
}
rootCA := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix() + 1),
Subject: pkixName,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
MaxPathLenZero: false,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
interPrivateKey, interPublicKey, privateBytes, err := createPrivateKey(create.KeyType)
if err != nil {
return nil, err
}
ca.PrivateKey = string(privateBytes)
rootDer, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, interPublicKey, interPrivateKey)
if err != nil {
return nil, err
}
rootCert, err := x509.ParseCertificate(rootDer)
if err != nil {
return nil, err
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: rootCert.Raw,
}
pemData := pem.EncodeToMemory(certBlock)
ca.CSR = string(pemData)
if err := websiteCARepo.Create(context.Background(), ca); err != nil {
return nil, err
}
return &create, nil
}
func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) {
res := &response.WebsiteCADTO{}
ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return nil, err
}
res.WebsiteCA = ca
certBlock, _ := pem.Decode([]byte(ca.CSR))
if certBlock == nil {
return nil, buserr.New("ErrSSLCertificateFormat")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, err
}
res.CommonName = cert.Issuer.CommonName
res.Organization = strings.Join(cert.Issuer.Organization, ",")
res.Country = strings.Join(cert.Issuer.Country, ",")
res.Province = strings.Join(cert.Issuer.Province, ",")
res.City = strings.Join(cert.Issuer.Locality, ",")
res.OrganizationUint = strings.Join(cert.Issuer.OrganizationalUnit, ",")
return res, nil
}
func (w WebsiteCAService) Delete(id uint) error {
ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByCAID(id))
if len(ssls) > 0 {
return buserr.New("ErrDeleteCAWithSSL")
}
exist, err := websiteCARepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return err
}
if exist.Name == "1Panel" {
return buserr.New("ErrDefaultCA")
}
return websiteCARepo.DeleteBy(commonRepo.WithByID(id))
}
func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) {
var (
domains []string
ips []net.IP
websiteSSL = &model.WebsiteSSL{}
err error
ca model.WebsiteCA
)
if req.Renew {
websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return nil, err
}
ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(websiteSSL.CaID))
if err != nil {
return nil, err
}
existDomains := []string{websiteSSL.PrimaryDomain}
if websiteSSL.Domains != "" {
existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...)
}
for _, domain := range existDomains {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
}
}
} else {
ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return nil, err
}
websiteSSL = &model.WebsiteSSL{
Provider: constant.SelfSigned,
KeyType: req.KeyType,
PushDir: req.PushDir,
CaID: ca.ID,
AutoRenew: req.AutoRenew,
Description: req.Description,
ExecShell: req.ExecShell,
}
if req.ExecShell {
websiteSSL.Shell = req.Shell
}
if req.PushDir {
if !files.NewFileOp().Stat(req.Dir) {
return nil, buserr.New(constant.ErrLinkPathNotFound)
}
websiteSSL.Dir = req.Dir
}
if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n")
for _, domain := range domainArray {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
if !common.IsValidDomain(domain) {
err = buserr.WithName("ErrDomainFormat", domain)
return nil, err
}
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
}
}
if len(domains) > 0 {
websiteSSL.PrimaryDomain = domains[0]
websiteSSL.Domains = strings.Join(domains[1:], ",")
}
ipStrings := make([]string, len(ips))
for i, ip := range ips {
ipStrings[i] = ip.String()
}
if websiteSSL.PrimaryDomain == "" && len(ips) > 0 {
websiteSSL.PrimaryDomain = ipStrings[0]
ipStrings = ipStrings[1:]
}
if len(ipStrings) > 0 {
if websiteSSL.Domains != "" {
websiteSSL.Domains += ","
}
websiteSSL.Domains += strings.Join(ipStrings, ",")
}
}
}
rootCertBlock, _ := pem.Decode([]byte(ca.CSR))
if rootCertBlock == nil {
return nil, buserr.New("ErrSSLCertificateFormat")
}
rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes)
if err != nil {
return nil, err
}
rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey))
if rootPrivateKeyBlock == nil {
return nil, buserr.New("ErrSSLCertificateFormat")
}
var rootPrivateKey any
if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 {
rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes)
if err != nil {
return nil, err
}
} else {
rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes)
if err != nil {
return nil, err
}
}
interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType)
if err != nil {
return nil, err
}
notAfter := time.Now()
if req.Unit == "year" {
notAfter = notAfter.AddDate(req.Time, 0, 0)
} else {
notAfter = notAfter.AddDate(0, 0, req.Time)
}
interCsr := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix() + 2),
Subject: rootCsr.Subject,
NotBefore: time.Now(),
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey)
if err != nil {
return nil, err
}
interCert, err := x509.ParseCertificate(interDer)
if err != nil {
return nil, err
}
interCertBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: interCert.Raw,
}
_, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType)
if err != nil {
return nil, err
}
commonName := ""
if len(domains) > 0 {
commonName = domains[0]
}
if len(ips) > 0 {
commonName = ips[0].String()
}
subject := rootCsr.Subject
subject.CommonName = commonName
csr := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix() + 3),
Subject: subject,
NotBefore: time.Now(),
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: false,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: domains,
IPAddresses: ips,
}
der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock))
websiteSSL.PrivateKey = string(privateKeyBytes)
websiteSSL.ExpireDate = cert.NotAfter
websiteSSL.StartDate = cert.NotBefore
websiteSSL.Type = cert.Issuer.CommonName
websiteSSL.Organization = rootCsr.Subject.Organization[0]
websiteSSL.Status = constant.SSLReady
if req.Renew {
if err := websiteSSLRepo.Save(websiteSSL); err != nil {
return nil, err
}
} else {
if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
return nil, err
}
}
logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)
logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}))
saveCertificateFile(websiteSSL, logger)
if websiteSSL.ExecShell {
workDir := constant.DataDir
if websiteSSL.PushDir {
workDir = websiteSSL.Dir
}
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil {
logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()}))
} else {
logger.Println(i18n.GetMsgByKey("ExecShellSuccess"))
}
}
return websiteSSL, nil
}
func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType))
if err != nil {
return
}
var (
caPrivateKeyPEM = new(bytes.Buffer)
)
if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 {
publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey
publicKey = publicKey.(*ecdsa.PublicKey)
block := &pem.Block{
Type: "EC PRIVATE KEY",
}
privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey))
if sErr != nil {
err = sErr
return
}
block.Bytes = privateBytes
_ = pem.Encode(caPrivateKeyPEM, block)
} else {
publicKey = &privateKey.(*rsa.PrivateKey).PublicKey
_ = pem.Encode(caPrivateKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)),
})
}
privateKeyBytes = caPrivateKeyPEM.Bytes()
return
}
func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return nil, err
}
fileOp := files.NewFileOp()
dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", ca.Name)
if fileOp.Stat(dir) {
if err = fileOp.DeleteDir(dir); err != nil {
return nil, err
}
}
if err = fileOp.CreateDir(dir, 0666); err != nil {
return nil, err
}
if err = fileOp.WriteFile(path.Join(dir, "ca.csr"), strings.NewReader(ca.CSR), 0644); err != nil {
return nil, err
}
if err = fileOp.WriteFile(path.Join(dir, "private.key"), strings.NewReader(ca.PrivateKey), 0644); err != nil {
return nil, err
}
fileName := ca.Name + ".zip"
if err = fileOp.Compress([]string{path.Join(dir, "ca.csr"), path.Join(dir, "private.key")}, dir, fileName, files.SdkZip, ""); err != nil {
return nil, err
}
return os.Open(path.Join(dir, fileName))
}