mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
315 lines
8.5 KiB
Go
315 lines
8.5 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ssl_certificate
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/eggsampler/acme/v3"
|
|
|
|
"yunion.io/x/cloudmux/pkg/cloudprovider"
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
|
|
"yunion.io/x/onecloud/pkg/apis"
|
|
api "yunion.io/x/onecloud/pkg/apis/compute"
|
|
"yunion.io/x/onecloud/pkg/appsrv"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
|
"yunion.io/x/onecloud/pkg/compute/models"
|
|
"yunion.io/x/onecloud/pkg/compute/options"
|
|
"yunion.io/x/onecloud/pkg/util/logclient"
|
|
)
|
|
|
|
var (
|
|
SSLCertificateCreateWorkerManager *appsrv.SWorkerManager
|
|
)
|
|
|
|
func init() {
|
|
SSLCertificateCreateWorkerManager = appsrv.NewWorkerManager("SSLCertificateCreateWorkerManager", 8, 1024, false)
|
|
taskman.RegisterTaskAndWorker(SSLCertificateCreateTask{}, SSLCertificateCreateWorkerManager)
|
|
}
|
|
|
|
type SSLCertificateCreateTask struct {
|
|
taskman.STask
|
|
}
|
|
|
|
func (self *SSLCertificateCreateTask) taskFailed(ctx context.Context, sc *models.SSSLCertificate, err error) {
|
|
sc.SetStatus(ctx, self.UserCred, apis.STATUS_CREATE_FAILED, err.Error())
|
|
db.OpsLog.LogEvent(sc, db.ACT_ALLOCATE_FAIL, err, self.UserCred)
|
|
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_ALLOCATE, err, self.UserCred, false)
|
|
self.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
|
|
}
|
|
|
|
func key2pem(certKey *ecdsa.PrivateKey) ([]byte, error) {
|
|
certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "MarshalECPrivateKey")
|
|
}
|
|
|
|
return pem.EncodeToMemory(&pem.Block{
|
|
Type: "EC PRIVATE KEY",
|
|
Bytes: certKeyEnc,
|
|
}), nil
|
|
}
|
|
|
|
func (self *SSLCertificateCreateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
|
|
sc := obj.(*models.SSSLCertificate)
|
|
|
|
zone, err := sc.GetDnsZone()
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "GetDnsZone"))
|
|
return
|
|
}
|
|
|
|
iZone, err := zone.GetICloudDnsZone(ctx)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "GetProvider"))
|
|
return
|
|
}
|
|
if len(sc.Issuer) == 0 {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "Issuer is required"))
|
|
return
|
|
}
|
|
|
|
addr := ""
|
|
switch sc.Issuer {
|
|
case api.SSL_ISSUER_LETSENCRYPT:
|
|
addr = acme.LetsEncryptStaging
|
|
case api.SSL_ISSUER_ZEROSSL:
|
|
addr = acme.ZeroSSLProduction
|
|
}
|
|
|
|
client, err := acme.NewClient(addr)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewClient"))
|
|
return
|
|
}
|
|
client.PollInterval = 10 * time.Second
|
|
client.PollTimeout = 6 * time.Minute
|
|
|
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
|
|
return
|
|
}
|
|
|
|
emails := []string{}
|
|
for _, email := range options.Options.SSLAccounts {
|
|
emails = append(emails, "mailto:"+email)
|
|
}
|
|
account, err := client.NewAccount(privKey, false, true, emails...)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewAccount"))
|
|
return
|
|
}
|
|
|
|
domainList := strings.Split(sc.Sans, ",")
|
|
var ids []acme.Identifier
|
|
for _, domain := range domainList {
|
|
ids = append(ids, acme.Identifier{Type: "dns", Value: domain})
|
|
}
|
|
|
|
order, err := client.NewOrder(account, ids)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewOrder"))
|
|
return
|
|
}
|
|
|
|
for _, authUrl := range order.Authorizations {
|
|
auth, err := client.FetchAuthorization(account, authUrl)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchAuthorization"))
|
|
return
|
|
}
|
|
chal, ok := auth.ChallengeMap[acme.ChallengeTypeDNS01]
|
|
if !ok {
|
|
self.taskFailed(ctx, sc, fmt.Errorf("ChallengeTypeDNS01 not found"))
|
|
return
|
|
}
|
|
|
|
txt := acme.EncodeDNS01KeyAuthorization(chal.KeyAuthorization)
|
|
|
|
info := strings.Split(auth.Identifier.Value, ".")
|
|
subDomain := strings.Join(info[:len(info)-2], ".")
|
|
subDomain = strings.ReplaceAll(subDomain, "*", "")
|
|
dnsName := "_acme-challenge"
|
|
if len(subDomain) > 0 {
|
|
dnsName = dnsName + "." + subDomain
|
|
}
|
|
|
|
log.Debugf("add dns record for %s: label: %s, txt: %s", auth.Identifier.Value, dnsName, txt)
|
|
|
|
err = func() error {
|
|
opts := &cloudprovider.DnsRecord{
|
|
DnsName: dnsName,
|
|
DnsValue: txt,
|
|
DnsType: cloudprovider.DnsTypeTXT,
|
|
Enabled: true,
|
|
Ttl: 60,
|
|
}
|
|
recordId, err := iZone.AddDnsRecord(opts)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "AddDnsRecord")
|
|
}
|
|
|
|
cloudprovider.Wait(10*time.Second, 3*time.Minute, func() (bool, error) {
|
|
v, err := net.LookupTXT("_acme-challenge." + auth.Identifier.Value)
|
|
log.Debugf("lookup txt for %s: %s, error: %v", "_acme-challenge."+auth.Identifier.Value, v, err)
|
|
if len(v) > 0 {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
|
|
defer func() {
|
|
record, err := iZone.GetIDnsRecordById(recordId)
|
|
if err != nil {
|
|
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "GetIDnsRecordById"), self.UserCred, false)
|
|
return
|
|
}
|
|
err = record.Delete()
|
|
if err != nil {
|
|
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "Delete"), self.UserCred, false)
|
|
return
|
|
}
|
|
}()
|
|
|
|
chal, err = client.UpdateChallenge(account, chal)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "UpdateChallenge")
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "AddDnsRecord"))
|
|
return
|
|
}
|
|
}
|
|
|
|
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
|
|
return
|
|
}
|
|
|
|
b, err := key2pem(certKey)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "key2pem"))
|
|
return
|
|
}
|
|
|
|
tpl := &x509.CertificateRequest{
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
PublicKeyAlgorithm: x509.ECDSA,
|
|
PublicKey: certKey.Public(),
|
|
Subject: pkix.Name{CommonName: domainList[0]},
|
|
DNSNames: domainList,
|
|
}
|
|
csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "CreateCertificateRequest"))
|
|
return
|
|
}
|
|
csr, err := x509.ParseCertificateRequest(csrDer)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "ParseCertificateRequest"))
|
|
return
|
|
}
|
|
|
|
order, err = client.FinalizeOrder(account, order, csr)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "FinalizeOrder"))
|
|
return
|
|
}
|
|
|
|
certs, err := client.FetchCertificates(account, order.Certificate)
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchCertificates"))
|
|
return
|
|
}
|
|
|
|
start, end, country, province, city := time.Time{}, time.Time{}, "", "", ""
|
|
var pemData []string
|
|
for _, c := range certs {
|
|
start = c.NotBefore
|
|
end = c.NotAfter
|
|
if len(c.Subject.Country) > 0 {
|
|
country = c.Subject.Country[0]
|
|
}
|
|
if len(c.Subject.Province) > 0 {
|
|
province = c.Subject.Province[0]
|
|
}
|
|
if len(c.Subject.Locality) > 0 {
|
|
city = c.Subject.Locality[0]
|
|
}
|
|
pemData = append(pemData, strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: c.Raw,
|
|
}))))
|
|
}
|
|
|
|
_, err = db.Update(sc, func() error {
|
|
sc.Certificate = strings.Join(pemData, "\n")
|
|
sc.PrivateKey = string(b)
|
|
sc.EndDate = end
|
|
sc.StartDate = start
|
|
sc.Country = country
|
|
sc.Province = province
|
|
sc.City = city
|
|
sc.Status = apis.STATUS_AVAILABLE
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
self.taskFailed(ctx, sc, errors.Wrapf(err, "Update"))
|
|
return
|
|
}
|
|
|
|
err = func() error {
|
|
provider, err := zone.GetProvider(ctx)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "GetProvider")
|
|
}
|
|
opts := &cloudprovider.SSLCertificateCreateOptions{
|
|
Certificate: sc.Certificate,
|
|
PrivateKey: sc.PrivateKey,
|
|
}
|
|
_, err = provider.CreateISSLCertificate(opts)
|
|
if err != nil {
|
|
if errors.Cause(err) == cloudprovider.ErrNotImplemented || errors.Cause(err) == cloudprovider.ErrNotSupported {
|
|
return nil
|
|
}
|
|
return errors.Wrapf(err, "CreateISSLCertificate")
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_CREATE, err, self.UserCred, false)
|
|
}
|
|
|
|
self.SetStageComplete(ctx, nil)
|
|
}
|