diff --git a/cmd/climc/shell/compute/sslcertificate.go b/cmd/climc/shell/compute/sslcertificate.go index bb72da4727..ebd4b7230b 100644 --- a/cmd/climc/shell/compute/sslcertificate.go +++ b/cmd/climc/shell/compute/sslcertificate.go @@ -24,6 +24,7 @@ import ( func init() { cmd := shell.NewResourceCmd(&modules.SslCerticicate).WithKeyword("ssl-certificate") cmd.List(&compute.SslCertificateListOptions{}) + cmd.Create(&compute.SslCertificateCreateOptions{}) cmd.Show(&options.BaseShowOptions{}) cmd.Delete(&options.BaseIdOptions{}) } diff --git a/go.mod b/go.mod index ab51cca353..83f2c2946d 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/creack/pty v1.1.18 github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 + github.com/eggsampler/acme/v3 v3.6.2 github.com/fernet/fernet-go v0.0.0-20180830025343-9eac43b88a5e github.com/fsnotify/fsnotify v1.4.9 github.com/ghodss/yaml v1.0.0 @@ -95,12 +96,12 @@ require ( k8s.io/cri-api v0.22.17 k8s.io/klog/v2 v2.20.0 moul.io/http2curl/v2 v2.3.0 - yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574 + yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0 yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1 yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91 yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 - yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0 + yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6 yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 yunion.io/x/sqlchemy v1.1.3-0.20250531010554-ce98f840b833 yunion.io/x/structarg v0.0.0-20231017124457-df4d5009457c diff --git a/go.sum b/go.sum index bae9b113b8..603cd46a4e 100644 --- a/go.sum +++ b/go.sum @@ -295,6 +295,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eggsampler/acme/v3 v3.6.2 h1:gvyZbQ92wNQLDASVftGpHEdFwPSfg0+17P0lLt09Tp8= +github.com/eggsampler/acme/v3 v3.6.2/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a h1:2Zw3pxDRTs4nX1WCLAEm27UN0hvjZSge7EaUUQexRZw= github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a/go.mod h1:afkYpY8JAIL4341N7Zj9xJ5yTovsg6BkWfBFlCzIoF4= @@ -1406,8 +1408,8 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574 h1:kglRbPc1P9I7rt3DP7O4pc8ji2JqVvZ4GSD+j52fwSI= -yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574/go.mod h1:FXxAEbdNfWXX9gjME3K2nJhkydHY5EKEUZb+RLEzVwQ= +yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed h1:L34xL83eV2RzpwCjfzOE7ls/5u5EvEjqAL2ToTaD6Tc= +yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed/go.mod h1:FXxAEbdNfWXX9gjME3K2nJhkydHY5EKEUZb+RLEzVwQ= yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0 h1:msG4SiDSVU7CrXH06WuHlNEZXIooTcmNbfrIGHuIHBU= yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws= yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634= @@ -1421,8 +1423,8 @@ yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 h1:Hu/4ERvoWaN6aiFs4h4/yvVB yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc= yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= -yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0 h1:diTsDq4t6jntGdAKtHZYx9eRWkLg0KyMTp0WFdueT+0= -yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM= +yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6 h1:CzEjNUur33HGmDkW7uKw1eU97gwojNsXlB2AMEzItn8= +yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM= yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4= yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo= yunion.io/x/sqlchemy v1.1.3-0.20250531010554-ce98f840b833 h1:XTFC1naKYkciCQDLm9izpzHXfTenmmtYsTpVKrsN5hE= diff --git a/pkg/apis/compute/sslcertificate.go b/pkg/apis/compute/sslcertificate.go index 8a16e94c03..d3d5da8cdf 100644 --- a/pkg/apis/compute/sslcertificate.go +++ b/pkg/apis/compute/sslcertificate.go @@ -14,10 +14,31 @@ package compute -import "yunion.io/x/onecloud/pkg/apis" +import ( + "yunion.io/x/cloudmux/pkg/cloudprovider" + + "yunion.io/x/onecloud/pkg/apis" +) + +const ( + SSL_ISSUER_LETSENCRYPT = cloudprovider.SSL_ISSUER_LETSENCRYPT + SSL_ISSUER_ZEROSSL = cloudprovider.SSL_ISSUER_ZEROSSL +) -// 资源创建参数, 目前仅占位 type SSLCertificateCreateInput struct { + apis.VirtualResourceCreateInput + + Issuer string `json:"issuer"` + DnsZoneId string `json:"dns_zone_id"` + Sans string `json:"sans"` + + Province string + Common string + Country string + City string + OrgName string + Certificate string + PrivateKey string } // 资源返回详情 diff --git a/pkg/compute/models/sslcertificate.go b/pkg/compute/models/sslcertificate.go index f0b55f23b9..e3db60c001 100644 --- a/pkg/compute/models/sslcertificate.go +++ b/pkg/compute/models/sslcertificate.go @@ -24,10 +24,13 @@ import ( "yunion.io/x/pkg/util/compare" "yunion.io/x/sqlchemy" + "yunion.io/x/onecloud/pkg/apis" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" + "yunion.io/x/onecloud/pkg/cloudcommon/validators" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/stringutils2" @@ -67,19 +70,18 @@ type SSSLCertificate struct { SDeletePreventableResourceBase Sans string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - StartDate time.Time `nullable:"false" list:"user" json:"start_date"` - Province string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - Common string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - Country string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - Issuer string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - Expired bool `charset:"utf8" nullable:"false" list:"user" create:"required"` - IsUpload bool `charset:"utf8" nullable:"false" list:"user" create:"required"` - EndDate time.Time `nullable:"false" list:"user" json:"end_date"` - Fingerprint string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"` - City string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - OrgName string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"` - Certificate string `charset:"utf8" nullable:"true" list:"user" create:"required"` - PrivateKey string `charset:"utf8" nullable:"true" list:"user" create:"required"` + StartDate time.Time `list:"user"` + Province string `width:"64" charset:"utf8" nullable:"false" list:"user" create:"optional"` + Common string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"optional"` + Country string `width:"32" charset:"utf8" nullable:"false" list:"user" create:"optional"` + Issuer string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"` + IsUpload bool `list:"user" create:"optional"` + EndDate time.Time `list:"user" create:"optional"` + Fingerprint string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"optional"` + City string `width:"64" charset:"utf8" nullable:"false" list:"user" create:"optional"` + OrgName string `width:"256" charset:"utf8" nullable:"false" list:"user" create:"optional"` + Certificate string `charset:"utf8" nullable:"true" list:"user" create:"optional"` + PrivateKey string `charset:"utf8" nullable:"true" list:"user" create:"optional"` } func (s SSSLCertificate) GetExternalId() string { @@ -188,8 +190,53 @@ func (man *SSSLCertificateManager) QueryDistinctExtraField(q *sqlchemy.SQuery, f return q, httperrors.ErrNotFound } -func (man *SSSLCertificateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.SSLCertificateCreateInput) (api.SSLCertificateCreateInput, error) { - return input, httperrors.NewNotImplementedError("Not Implemented") +func (man *SSSLCertificateManager) ValidateCreateData( + ctx context.Context, + userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, + query jsonutils.JSONObject, + input *api.SSLCertificateCreateInput, +) (*api.SSLCertificateCreateInput, error) { + if len(input.DnsZoneId) > 0 { + obj, err := validators.ValidateModel(ctx, userCred, DnsZoneManager, &input.DnsZoneId) + if err != nil { + return nil, err + } + input.DnsZoneId = obj.GetId() + } + var err error + input.VirtualResourceCreateInput, err = man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.VirtualResourceCreateInput) + if err != nil { + return nil, err + } + + return input, nil +} + +func (self *SSSLCertificate) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + self.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data) + self.StartSSLCertificateCreateTask(ctx, userCred, "") +} + +func (self *SSSLCertificate) StartSSLCertificateCreateTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + params := jsonutils.NewDict() + task, err := taskman.TaskManager.NewTask(ctx, "SSLCertificateCreateTask", self, userCred, params, parentTaskId, "", nil) + if err != nil { + return errors.Wrap(err, "NewTask") + } + self.SetStatus(ctx, userCred, apis.STATUS_CREATING, "") + return task.ScheduleRun(nil) +} + +func (self *SSSLCertificate) GetDnsZone() (*SDnsZone, error) { + if len(self.DnsZoneId) == 0 { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "DnsZoneId is empty") + } + zone, err := DnsZoneManager.FetchById(self.DnsZoneId) + if err != nil { + return nil, errors.Wrapf(err, "DnsZoneManager.FetchById") + } + return zone.(*SDnsZone), nil } func (r *SCloudprovider) GetSSLCertificates() ([]SSSLCertificate, error) { @@ -290,7 +337,7 @@ func (s *SSSLCertificate) SyncWithCloudSSLCertificate(ctx context.Context, userC s.Common = ext.GetCommon() s.Country = ext.GetCountry() s.Issuer = ext.GetIssuer() - s.Expired = ext.GetExpired() + //s.Expired = ext.GetExpired() s.IsUpload = ext.GetIsUpload() s.EndDate = ext.GetEndDate() s.Fingerprint = ext.GetFingerprint() @@ -349,7 +396,7 @@ func (r *SCloudprovider) newFromCloudSSLCertificate( s.Common = ext.GetCommon() s.Country = ext.GetCountry() s.Issuer = ext.GetIssuer() - s.Expired = ext.GetExpired() + //s.Expired = ext.GetExpired() s.IsUpload = ext.GetIsUpload() s.EndDate = ext.GetEndDate() s.Fingerprint = ext.GetFingerprint() diff --git a/pkg/compute/options/options.go b/pkg/compute/options/options.go index d3283128e6..f39708dd2a 100644 --- a/pkg/compute/options/options.go +++ b/pkg/compute/options/options.go @@ -235,6 +235,8 @@ type ComputeOptions struct { NetworkAlwaysManualConfig bool `help:"always manually configure network settings" default:"false"` + SSLAccounts []string `help:"SSL account"` + ComputeEEOptions } diff --git a/pkg/compute/tasks/loader.go b/pkg/compute/tasks/loader.go index 86a0c1147f..e1ce70513a 100644 --- a/pkg/compute/tasks/loader.go +++ b/pkg/compute/tasks/loader.go @@ -48,6 +48,7 @@ import ( _ "yunion.io/x/onecloud/pkg/compute/tasks/server_sku" _ "yunion.io/x/onecloud/pkg/compute/tasks/snapshot" _ "yunion.io/x/onecloud/pkg/compute/tasks/snapshotpolicy" + _ "yunion.io/x/onecloud/pkg/compute/tasks/ssl_certificate" _ "yunion.io/x/onecloud/pkg/compute/tasks/storage" _ "yunion.io/x/onecloud/pkg/compute/tasks/vpc" _ "yunion.io/x/onecloud/pkg/compute/tasks/vpc_peering" diff --git a/pkg/compute/tasks/ssl_certificate/doc.go b/pkg/compute/tasks/ssl_certificate/doc.go new file mode 100644 index 0000000000..e7351abf73 --- /dev/null +++ b/pkg/compute/tasks/ssl_certificate/doc.go @@ -0,0 +1 @@ +package ssl_certificate // import "yunion.io/x/onecloud/pkg/compute/tasks/ssl_certificate" diff --git a/pkg/compute/tasks/ssl_certificate/ssl_certificate_create_task.go b/pkg/compute/tasks/ssl_certificate/ssl_certificate_create_task.go new file mode 100644 index 0000000000..163de34fb1 --- /dev/null +++ b/pkg/compute/tasks/ssl_certificate/ssl_certificate_create_task.go @@ -0,0 +1,314 @@ +// 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) +} diff --git a/pkg/mcclient/options/compute/sslcerticicate.go b/pkg/mcclient/options/compute/sslcerticicate.go index 119cb0b973..a02d4bbe53 100644 --- a/pkg/mcclient/options/compute/sslcerticicate.go +++ b/pkg/mcclient/options/compute/sslcerticicate.go @@ -27,3 +27,23 @@ type SslCertificateListOptions struct { func (opts *SslCertificateListOptions) Params() (jsonutils.JSONObject, error) { return options.ListStructToParams(opts) } + +type SslCertificateCreateOptions struct { + options.BaseCreateOptions + + Issuer string `json:"issuer"` + DnsZoneId string `json:"dns_zone_id"` + Sans string `json:"sans"` + + Province string + Common string + Country string + City string + OrgName string + Certificate string + PrivateKey string +} + +func (opts *SslCertificateCreateOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +} diff --git a/vendor/github.com/eggsampler/acme/v3/.gitignore b/vendor/github.com/eggsampler/acme/v3/.gitignore new file mode 100644 index 0000000000..236968e66d --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/.gitignore @@ -0,0 +1,3 @@ +.idea/ +*.out +coverage* diff --git a/vendor/github.com/eggsampler/acme/v3/LICENSE b/vendor/github.com/eggsampler/acme/v3/LICENSE new file mode 100644 index 0000000000..b9a4b36556 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Isaac + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/eggsampler/acme/v3/Makefile b/vendor/github.com/eggsampler/acme/v3/Makefile new file mode 100644 index 0000000000..1f56d9b9b9 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/Makefile @@ -0,0 +1,75 @@ + +.PHONY: test examples clean test_full pebble pebble_setup pebble_start pebble_wait pebble_stop boulder boulder_setup boulder_start boulder_stop + +# some variables for path injection, if already set will not override +GOPATH ?= $(HOME)/go +BOULDER_PATH ?= $(GOPATH)/src/github.com/letsencrypt/boulder +PEBBLE_PATH ?= $(GOPATH)/src/github.com/letsencrypt/pebble +TEST_PATH ?= github.com/eggsampler/acme/v3 +CLIENT ?= unknown + +# tests the code against an already running ca instance +# to actually do a test against pebble or boulder, including , see the 'pebble' or 'boulder' targets +test: + -go clean -testcache + CGO_ENABLED=1 go test -v -race -coverprofile=coverage-$(CLIENT).out -covermode=atomic $(TEST_PATH) + +examples: + go build -o /dev/null examples/certbot/certbot.go + go build -o /dev/null examples/autocert/autocert.go + go build -o /dev/null examples/zerossl/zerossl.go + go build -o /dev/null examples/ari/renewalinfo.go + +clean: + rm -f coverage*.out + +test_full: clean examples pebble pebble_stop boulder boulder_stop + + +# sets up & runs pebble (in docker), tests, then stops pebble +pebble: CLIENT = pebble +pebble: pebble_setup pebble_start pebble_wait test pebble_stop + +pebble_setup: + CLIENT=pebble + mkdir -p $(PEBBLE_PATH) + -git clone --depth 1 https://github.com/letsencrypt/pebble.git $(PEBBLE_PATH) + (cd $(PEBBLE_PATH); git checkout -f main && git reset --hard HEAD && git pull -q) + make pebble_stop + +# runs an instance of pebble using docker +pebble_start: + docker-compose -f $(PEBBLE_PATH)/docker-compose.yml up -d + +# waits until pebble responds +pebble_wait: + while ! wget --delete-after -q --no-check-certificate "https://localhost:14000/dir" ; do sleep 1 ; done + +# stops the running pebble instance +pebble_stop: + docker-compose -f $(PEBBLE_PATH)/docker-compose.yml down + + +# sets up & runs boulder (in docker), tests, then stops boulder +boulder: CLIENT = boulder +boulder: boulder_setup boulder_start boulder_wait test boulder_stop + +# NB: this edits docker-compose.yml +boulder_setup: + mkdir -p $(BOULDER_PATH) + -git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH) + (cd $(BOULDER_PATH); git checkout -f main && git reset --hard HEAD && git pull -q) + make boulder_stop + (cd $(BOULDER_PATH); docker compose run --rm bsetup) + +# runs an instance of boulder +boulder_start: + docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml up -d + +# waits until boulder responds +boulder_wait: + while ! wget --delete-after -q --no-check-certificate "http://localhost:4001/directory" ; do sleep 1 ; done + +# stops the running docker instance +boulder_stop: + docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml down diff --git a/vendor/github.com/eggsampler/acme/v3/README.md b/vendor/github.com/eggsampler/acme/v3/README.md new file mode 100644 index 0000000000..24f9f1b026 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/README.md @@ -0,0 +1,43 @@ +# eggsampler/acme + +[![GoDoc](https://godoc.org/github.com/eggsampler/acme?status.svg)](https://godoc.org/github.com/eggsampler/acme) +[![Build Status](https://github.com/eggsampler/acme/actions/workflows/go.yml/badge.svg)](https://github.com/eggsampler/acme/actions) +[![Coverage Status](https://coveralls.io/repos/github/eggsampler/acme/badge.svg)](https://coveralls.io/github/eggsampler/acme) + +## About + +`eggsampler/acme` is a Go client library implementation for [RFC8555](https://tools.ietf.org/html/rfc8555) (previously ACME v2). This library can be used with the [Let's Encrypt](https://letsencrypt.org/) Certificate Authority (CA), but also other ACME compliant CA's such as [ZeroSSL](https://zerossl.com/). + +The library is designed to provide a zero external dependency wrapper over exposed directory endpoints and provide objects in easy to use structures. + +## Requirements + +A Go version of at least 1.11 is required as this repository is designed to be imported as a Go module. + +## Usage + +Simply import the module into a project, + +```go +import "github.com/eggsampler/acme/v3" +``` + +Note the `/v3` major version at the end. Due to the way modules function, this is the major version as represented in the `go.mod` file and latest git repo [semver](https://semver.org/) tag. +All functions are still exported and called using the `acme` package name. + +## Examples + +A simple [certbot](https://certbot.eff.org/)-like example is provided in the examples/certbot directory. +This code demonstrates account registration, new order submission, fulfilling challenges, finalising an order and fetching the issued certificate chain. + +An example of how to use the autocert package is also provided in examples/autocert. + +## Tests + +The tests can be run against an instance of [boulder](https://github.com/letsencrypt/boulder) or [pebble](https://github.com/letsencrypt/pebble). + +Challenge fulfilment is designed to use the new `challtestsrv` server present inside boulder and pebble which responds to dns queries and challenges as required. + +To run tests against an already running instance of boulder or pebble, use the `test` target in the Makefile. + +Some convenience targets for launching pebble/boulder using their respective docker compose files have also been included in the Makefile. diff --git a/vendor/github.com/eggsampler/acme/v3/THIRD-PARTY b/vendor/github.com/eggsampler/acme/v3/THIRD-PARTY new file mode 100644 index 0000000000..1c53d75443 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/THIRD-PARTY @@ -0,0 +1,35 @@ +This document contains Third Party Software Notices and/or Additional Terms and Conditions for licensed third party software components included within this product. + +== + +https://github.com/golang/crypto/blob/master/acme/jws.go +https://github.com/golang/crypto/blob/master/acme/jws_test.go +(with modifications) + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/github.com/eggsampler/acme/v3/account.go b/vendor/github.com/eggsampler/acme/v3/account.go new file mode 100644 index 0000000000..1c969824e8 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/account.go @@ -0,0 +1,144 @@ +package acme + +import ( + "crypto" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" +) + +// NewAccount registers a new account with the acme service +// Note this function is essentially deprecated and only present for backwards compatibility. +// New programs should implement NewAccountOptions instead. +func (c Client) NewAccount(privateKey crypto.Signer, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (Account, error) { + var opts []NewAccountOptionFunc + if onlyReturnExisting { + opts = append(opts, NewAcctOptOnlyReturnExisting()) + } + if termsOfServiceAgreed { + opts = append(opts, NewAcctOptAgreeTOS()) + } + if len(contact) > 0 { + opts = append(opts, NewAcctOptWithContacts(contact...)) + } + + return c.NewAccountOptions(privateKey, opts...) +} + +// NewAccountOptions registers an account with an acme server with the provided options. +func (c Client) NewAccountOptions(privateKey crypto.Signer, options ...NewAccountOptionFunc) (Account, error) { + newAccountReq := NewAccountRequest{} + account := Account{} + + for _, opt := range options { + if err := opt(privateKey, &account, &newAccountReq, c); err != nil { + return account, err + } + } + + resp, err := c.post(c.dir.NewAccount, "", privateKey, newAccountReq, &account, http.StatusOK, http.StatusCreated) + if err != nil { + return account, err + } + + account.URL = resp.Header.Get("Location") + account.PrivateKey = privateKey + + if account.Thumbprint == "" { + account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public()) + if err != nil { + return account, fmt.Errorf("acme: error computing account thumbprint: %v", err) + } + } + + return account, nil +} + +// UpdateAccount updates an existing account with the acme service. +func (c Client) UpdateAccount(account Account, contact ...string) (Account, error) { + var updateAccountReq interface{} + + if !reflect.DeepEqual(account.Contact, contact) { + // Only provide a non-nil updateAccountReq when there is an update to be made. + updateAccountReq = struct { + Contact []string `json:"contact,omitempty"` + }{ + Contact: contact, + } + } else { + // Otherwise use "" to trigger a POST-as-GET to fetch up-to-date account + // information from the acme service. + updateAccountReq = "" + } + + _, err := c.post(account.URL, account.URL, account.PrivateKey, updateAccountReq, &account, http.StatusOK) + if err != nil { + return account, err + } + + if account.Thumbprint == "" { + account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public()) + if err != nil { + return account, fmt.Errorf("acme: error computing account thumbprint: %v", err) + } + } + + return account, nil +} + +// AccountKeyChange rolls over an account to a new key. +func (c Client) AccountKeyChange(account Account, newPrivateKey crypto.Signer) (Account, error) { + oldJwkKeyPub, err := jwkEncode(account.PrivateKey.Public()) + if err != nil { + return account, fmt.Errorf("acme: error encoding new private key: %v", err) + } + + keyChangeReq := struct { + Account string `json:"account"` + OldKey json.RawMessage `json:"oldKey"` + }{ + Account: account.URL, + OldKey: []byte(oldJwkKeyPub), + } + + innerJws, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, "", "", c.dir.KeyChange) + if err != nil { + return account, fmt.Errorf("acme: error encoding inner jws: %v", err) + } + + if _, err := c.post(c.dir.KeyChange, account.URL, account.PrivateKey, json.RawMessage(innerJws), nil, http.StatusOK); err != nil { + return account, err + } + + account.PrivateKey = newPrivateKey + + return account, nil +} + +// DeactivateAccount deactivates a given account. +func (c Client) DeactivateAccount(account Account) (Account, error) { + deactivateReq := struct { + Status string `json:"status"` + }{ + Status: "deactivated", + } + + _, err := c.post(account.URL, account.URL, account.PrivateKey, deactivateReq, &account, http.StatusOK) + + return account, err +} + +// FetchOrderList fetches a list of orders from the account url provided in the account Orders field +func (c Client) FetchOrderList(account Account) (OrderList, error) { + orderList := OrderList{} + + if account.Orders == "" { + return orderList, errors.New("no order list for account") + } + + _, err := c.post(account.Orders, account.URL, account.PrivateKey, "", &orderList, http.StatusOK) + + return orderList, err +} diff --git a/vendor/github.com/eggsampler/acme/v3/acme.go b/vendor/github.com/eggsampler/acme/v3/acme.go new file mode 100644 index 0000000000..0c02cbdb1c --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/acme.go @@ -0,0 +1,303 @@ +package acme + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "regexp" + "strings" + "time" +) + +const ( + // LetsEncryptProduction holds the production directory url + LetsEncryptProduction = "https://acme-v02.api.letsencrypt.org/directory" + + // LetsEncryptStaging holds the staging directory url + LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" + + // ZeroSSLProduction holds the ZeroSSL directory url + ZeroSSLProduction = "https://acme.zerossl.com/v2/DV90" + + userAgentString = "eggsampler-acme/v3 Go-http-client/1.1" +) + +// NewClient creates a new acme client given a valid directory url. +func NewClient(directoryURL string, options ...OptionFunc) (Client, error) { + // Set a default http timeout of 60 seconds, this can be overridden + // via an OptionFunc eg: acme.NewClient(url, WithHTTPTimeout(10 * time.Second)) + httpClient := &http.Client{ + Timeout: 60 * time.Second, + } + + acmeClient := Client{ + httpClient: httpClient, + nonces: &nonceStack{}, + retryCount: 5, + } + + acmeClient.dir.URL = directoryURL + + for _, opt := range options { + if err := opt(&acmeClient); err != nil { + return acmeClient, fmt.Errorf("acme: error setting option: %v", err) + } + } + + if _, err := acmeClient.get(directoryURL, &acmeClient.dir, http.StatusOK); err != nil { + return acmeClient, err + } + + return acmeClient, nil +} + +// Directory is the object returned by the client connecting to a directory url. +func (c Client) Directory() Directory { + return c.dir +} + +// Helper function to get the poll interval and poll timeout, defaulting if 0 +func (c Client) getPollingDurations() (time.Duration, time.Duration) { + pollInterval := c.PollInterval + if pollInterval == 0 { + pollInterval = 500 * time.Millisecond + } + pollTimeout := c.PollTimeout + if pollTimeout == 0 { + pollTimeout = 30 * time.Second + } + return pollInterval, pollTimeout +} + +// Helper function to have a central point for performing http requests. Stores +// any returned nonces in the stack. The caller is responsible for closing the +// body so they can read the response. +func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) { + // identifier for this client, as well as the default go user agent + if c.userAgentSuffix != "" { + req.Header.Set("User-Agent", userAgentString+" "+c.userAgentSuffix) + } else { + req.Header.Set("User-Agent", userAgentString) + } + + if c.acceptLanguage != "" { + req.Header.Set("Accept-Language", c.acceptLanguage) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return resp, err + } + + if addNonce { + c.nonces.push(resp.Header.Get("Replay-Nonce")) + } + + return resp, nil +} + +// Helper function to perform an HTTP get request and read the body. The caller +// is responsible for closing the body so they can read the response. +func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byte, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, nil, fmt.Errorf("acme: error creating request: %v", err) + } + + resp, err := c.do(req, true) + if err != nil { + return resp, nil, fmt.Errorf("acme: error fetching response: %v", err) + } + defer resp.Body.Close() + + if err := checkError(resp, expectedStatus...); err != nil { + return resp, nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp, body, fmt.Errorf("acme: error reading response body: %v", err) + } + + return resp, body, nil +} + +// Helper function for performing a http get on an acme resource. The caller is +// responsible for closing the body so they can read the response. +func (c Client) get(url string, out interface{}, expectedStatus ...int) (*http.Response, error) { + resp, body, err := c.getRaw(url, expectedStatus...) + if err != nil { + return resp, err + } + + if len(body) > 0 && out != nil { + if err := json.Unmarshal(body, out); err != nil { + return resp, fmt.Errorf("acme: error parsing response body: %v", err) + } + } + + return resp, nil +} + +func (c Client) nonce() (string, error) { + nonce := c.nonces.pop() + if nonce != "" { + return nonce, nil + } + + if c.dir.NewNonce == "" { + return "", errors.New("acme: no new nonce url") + } + + req, err := http.NewRequest("HEAD", c.dir.NewNonce, nil) + if err != nil { + return "", fmt.Errorf("acme: error creating new nonce request: %v", err) + } + + resp, err := c.do(req, false) + if err != nil { + return "", fmt.Errorf("acme: error fetching new nonce: %v", err) + } + + nonce = resp.Header.Get("Replay-Nonce") + return nonce, nil +} + +// Helper function to perform an HTTP post request and read the body. Will +// attempt to retry if error is badNonce. The caller is responsible for closing +// the body so they can read the response. +func (c Client) postRaw(retryCount int, requestURL, kid string, privateKey crypto.Signer, payload interface{}, expectedStatus []int) (*http.Response, []byte, error) { + nonce, err := c.nonce() + if err != nil { + return nil, nil, err + } + + data, err := jwsEncodeJSON(payload, privateKey, KeyID(kid), nonce, requestURL) + if err != nil { + return nil, nil, fmt.Errorf("acme: error encoding json payload: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data)) + if err != nil { + return nil, nil, fmt.Errorf("acme: error creating request: %v", err) + } + req.Header.Set("Content-Type", "application/jose+json") + + resp, err := c.do(req, true) + if err != nil { + return resp, nil, fmt.Errorf("acme: error sending request: %v", err) + } + defer resp.Body.Close() + + if err := checkError(resp, expectedStatus...); err != nil { + prob, ok := err.(Problem) + if !ok { + // don't retry for an error we don't know about + return resp, nil, err + } + if retryCount >= c.retryCount { + // don't attempt to retry if too many retries + return resp, nil, err + } + if strings.HasSuffix(prob.Type, ":badNonce") { + // only retry if error is badNonce + return c.postRaw(retryCount+1, requestURL, kid, privateKey, payload, expectedStatus) + } + return resp, nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp, body, fmt.Errorf("acme: error reading response body: %v", err) + } + + return resp, body, nil +} + +// Helper function for performing a http post to an acme resource. The caller is +// responsible for closing the body so they can read the response. +func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) { + resp, body, err := c.postRaw(0, requestURL, keyID, privateKey, payload, expectedStatus) + if err != nil { + return resp, err + } + + if _, b := os.LookupEnv("ACME_DEBUG_POST"); b { + fmt.Println() + fmt.Println("========= " + requestURL) + fmt.Println(string(body)) + fmt.Println() + } + + if len(body) > 0 && out != nil { + if err := json.Unmarshal(body, out); err != nil { + return resp, fmt.Errorf("acme: error parsing response: %v - %s", err, string(body)) + } + } + + return resp, nil +} + +var regLink = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`) + +// Fetches a http Link header from an http response and closes the body. +func fetchLink(resp *http.Response, wantedLink string) string { + if resp == nil { + return "" + } + linkHeader := resp.Header["Link"] + if len(linkHeader) == 0 { + return "" + } + for _, l := range linkHeader { + matches := regLink.FindAllStringSubmatch(l, -1) + for _, m := range matches { + if len(m) != 3 { + continue + } + if m[2] == wantedLink { + return m[1] + } + } + } + return "" +} + +// Fetch is a helper function to assist with POST-AS-GET requests +func (c Client) Fetch(account Account, requestURL string, result interface{}, expectedStatus ...int) error { + if len(expectedStatus) == 0 { + expectedStatus = []int{http.StatusOK} + } + _, err := c.post(requestURL, account.URL, account.PrivateKey, "", result, expectedStatus...) + + return err +} + +// Fetches all http Link header from a http response +func fetchLinks(resp *http.Response, wantedLink string) []string { + if resp == nil { + return nil + } + linkHeader := resp.Header["Link"] + if len(linkHeader) == 0 { + return nil + } + var links []string + for _, l := range linkHeader { + matches := regLink.FindAllStringSubmatch(l, -1) + for _, m := range matches { + if len(m) != 3 { + continue + } + if m[2] == wantedLink { + links = append(links, m[1]) + } + } + } + return links +} diff --git a/vendor/github.com/eggsampler/acme/v3/ari.go b/vendor/github.com/eggsampler/acme/v3/ari.go new file mode 100644 index 0000000000..aad35e560b --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/ari.go @@ -0,0 +1,131 @@ +package acme + +import ( + _ "crypto/sha1" + _ "crypto/sha256" + _ "crypto/sha512" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "fmt" + "math/rand" + "net/http" + "strconv" + "strings" + "time" +) + +// GetRenewalInfo returns the renewal information (if present and supported by +// the ACME server), and a Retry-After time if indicated in the http response +// header. +func (c Client) GetRenewalInfo(cert *x509.Certificate) (RenewalInfo, error) { + if c.dir.RenewalInfo == "" { + return RenewalInfo{}, ErrRenewalInfoNotSupported + } + + certID, err := GenerateARICertID(cert) + if err != nil { + return RenewalInfo{}, fmt.Errorf("acme: error generating certificate id: %v", err) + } + + renewalURL := c.dir.RenewalInfo + if !strings.HasSuffix(renewalURL, "/") { + renewalURL += "/" + } + renewalURL += certID + var ri RenewalInfo + + resp, err := c.get(renewalURL, &ri, http.StatusOK) + if err != nil { + return ri, err + } + defer resp.Body.Close() + + ri.RetryAfter, err = parseRetryAfter(resp.Header.Get("Retry-After")) + return ri, err +} + +// GenerateARICertID constructs a certificate identifier as described in +// draft-ietf-acme-ari-03, section 4.1. +func GenerateARICertID(cert *x509.Certificate) (string, error) { + if cert == nil { + return "", fmt.Errorf("certificate not found") + } + + derBytes, err := asn1.Marshal(cert.SerialNumber) + if err != nil { + return "", err + } + + if len(derBytes) < 3 { + return "", fmt.Errorf("invalid DER encoding of serial number") + } + + // Extract only the integer bytes from the DER encoded Serial Number + // Skipping the first 2 bytes (tag and length). The result is base64url + // encoded without padding. + serial := base64.RawURLEncoding.EncodeToString(derBytes[2:]) + + // Convert the Authority Key Identifier to base64url encoding without + // padding. + aki := base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId) + + // Construct the final identifier by concatenating AKI and Serial Number. + return fmt.Sprintf("%s.%s", aki, serial), nil +} + +func (r RenewalInfo) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time { + // Explicitly convert all times to UTC. + now = now.UTC() + start := r.SuggestedWindow.Start.UTC() + end := r.SuggestedWindow.End.UTC() + + // Select a uniform random time within the suggested window. + window := end.Sub(start) + randomDuration := time.Duration(rand.Int63n(int64(window))) + randomTime := start.Add(randomDuration) + + // If the selected time is in the past, attempt renewal immediately. + if randomTime.Before(now) { + return &now + } + + // Otherwise, if the client can schedule itself to attempt renewal at + // exactly the selected time, do so. + willingToSleepUntil := now.Add(willingToSleep) + if willingToSleepUntil.After(randomTime) || willingToSleepUntil.Equal(randomTime) { + return &randomTime + } + + return nil +} + +// timeNow and implementations support testing +type timeNow interface { + Now() time.Time +} + +type currentTimeNow struct{} + +func (currentTimeNow) Now() time.Time { + return time.Now() +} + +var systemTime timeNow = currentTimeNow{} + +func parseRetryAfter(ra string) (time.Time, error) { + retryAfterString := strings.TrimSpace(ra) + if len(retryAfterString) == 0 { + return time.Time{}, nil + } + + if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterString); err == nil { + return retryAfterTime, nil + } + + if retryAfterInt, err := strconv.Atoi(retryAfterString); err == nil { + return systemTime.Now().Add(time.Second * time.Duration(retryAfterInt)), nil + } + + return time.Time{}, fmt.Errorf("invalid time format: %s", retryAfterString) +} diff --git a/vendor/github.com/eggsampler/acme/v3/authorization.go b/vendor/github.com/eggsampler/acme/v3/authorization.go new file mode 100644 index 0000000000..09d5906d98 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/authorization.go @@ -0,0 +1,43 @@ +package acme + +import "net/http" + +// FetchAuthorization fetches an authorization from an authorization url provided in an order. +func (c Client) FetchAuthorization(account Account, authURL string) (Authorization, error) { + authResp := Authorization{} + _, err := c.post(authURL, account.URL, account.PrivateKey, "", &authResp, http.StatusOK) + if err != nil { + return authResp, err + } + + for i := 0; i < len(authResp.Challenges); i++ { + if authResp.Challenges[i].KeyAuthorization == "" { + authResp.Challenges[i].KeyAuthorization = authResp.Challenges[i].Token + "." + account.Thumbprint + } + } + + authResp.ChallengeMap = map[string]Challenge{} + authResp.ChallengeTypes = []string{} + for _, c := range authResp.Challenges { + authResp.ChallengeMap[c.Type] = c + authResp.ChallengeTypes = append(authResp.ChallengeTypes, c.Type) + } + + authResp.URL = authURL + + return authResp, nil +} + +// DeactivateAuthorization deactivate a provided authorization url from an order. +func (c Client) DeactivateAuthorization(account Account, authURL string) (Authorization, error) { + deactivateReq := struct { + Status string `json:"status"` + }{ + Status: "deactivated", + } + deactivateResp := Authorization{} + + _, err := c.post(authURL, account.URL, account.PrivateKey, deactivateReq, &deactivateResp, http.StatusOK) + + return deactivateResp, err +} diff --git a/vendor/github.com/eggsampler/acme/v3/autocert.go b/vendor/github.com/eggsampler/acme/v3/autocert.go new file mode 100644 index 0000000000..1f3db59a8e --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/autocert.go @@ -0,0 +1,435 @@ +package acme + +// Similar to golang.org/x/crypto/acme/autocert + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" + "path" + "strings" + "sync" +) + +// HostCheck function prototype to implement for checking hosts against before issuing certificates +type HostCheck func(host string) error + +// WhitelistHosts implements a simple whitelist HostCheck +func WhitelistHosts(hosts ...string) HostCheck { + m := map[string]bool{} + for _, v := range hosts { + m[v] = true + } + + return func(host string) error { + if !m[host] { + return errors.New("autocert: host not whitelisted") + } + return nil + } +} + +// AutoCert is a stateful certificate manager for issuing certificates on connecting hosts +type AutoCert struct { + // Acme directory Url + // If nil, uses `LetsEncryptStaging` + DirectoryURL string + + // Options contains the options used for creating the acme client + Options []OptionFunc + + // A function to check whether a host is allowed or not + // If nil, all hosts allowed + // Use `WhitelistHosts(hosts ...string)` for a simple white list of hostnames + HostCheck HostCheck + + // Cache dir to store account data and certificates + // If nil, does not write cache data to file + CacheDir string + + // When using a staging environment, include a root certificate for verification purposes + RootCert string + + // Called before updating challenges + PreUpdateChallengeHook func(Account, Challenge) + + // Mapping of token -> keyauth + // Protected by a mutex, but not rwmutex because tokens are deleted once read + tokensLock sync.RWMutex + tokens map[string][]byte + + // Mapping of cache key -> value + cacheLock sync.Mutex + cache map[string][]byte + + // read lock around getting existing certs + // write lock around issuing new certificate + certLock sync.RWMutex + + client Client +} + +// HTTPHandler Wraps a handler and provides serving of http-01 challenge tokens from /.well-known/acme-challenge/ +// If handler is nil, will redirect all traffic otherwise to https +func (m *AutoCert) HTTPHandler(handler http.Handler) http.Handler { + if handler == nil { + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://"+r.Host+r.URL.RequestURI(), http.StatusMovedPermanently) + }) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { + handler.ServeHTTP(w, r) + return + } + + if err := m.checkHost(r.Host); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + token := path.Base(r.URL.Path) + m.tokensLock.RLock() + defer m.tokensLock.RUnlock() + keyAuth := m.tokens[token] + if len(keyAuth) == 0 { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + _, _ = w.Write(keyAuth) + }) +} + +// GetCertificate implements a tls.Config.GetCertificate hook +func (m *AutoCert) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + name := strings.TrimSuffix(hello.ServerName, ".") + + if name == "" { + return nil, errors.New("autocert: missing server name") + } + if !strings.Contains(strings.Trim(name, "."), ".") { + return nil, errors.New("autocert: server name component count invalid") + } + if strings.ContainsAny(name, `/\`) { + return nil, errors.New("autocert: server name contains invalid character") + } + + // check the hostname is allowed + if err := m.checkHost(name); err != nil { + return nil, err + } + + // check if there's an existing cert + m.certLock.RLock() + existingCert, _ := m.getExistingCert(name) + m.certLock.RUnlock() + if existingCert != nil { + return existingCert, nil + } + + // if not, attempt to issue a new cert + m.certLock.Lock() + defer m.certLock.Unlock() + return m.issueCert(name) +} + +func (m *AutoCert) getDirectoryURL() string { + if m.DirectoryURL != "" { + return m.DirectoryURL + } + + return LetsEncryptStaging +} + +func (m *AutoCert) getCache(keys ...string) []byte { + key := strings.Join(keys, "-") + + m.cacheLock.Lock() + defer m.cacheLock.Unlock() + + b := m.cache[key] + if len(b) > 0 { + return b + } + + if m.CacheDir == "" { + return nil + } + + b, _ = ioutil.ReadFile(path.Join(m.CacheDir, key)) + if len(b) == 0 { + return nil + } + + if m.cache == nil { + m.cache = map[string][]byte{} + } + m.cache[key] = b + return b +} + +func (m *AutoCert) putCache(data []byte, keys ...string) context.Context { + ctx, cancel := context.WithCancel(context.Background()) + + key := strings.Join(keys, "-") + + m.cacheLock.Lock() + defer m.cacheLock.Unlock() + + if m.cache == nil { + m.cache = map[string][]byte{} + } + m.cache[key] = data + + if m.CacheDir == "" { + cancel() + return ctx + } + + go func() { + _ = ioutil.WriteFile(path.Join(m.CacheDir, key), data, 0700) + cancel() + }() + + return ctx +} + +func (m *AutoCert) checkHost(name string) error { + if m.HostCheck == nil { + return nil + } + return m.HostCheck(name) +} + +func (m *AutoCert) getExistingCert(name string) (*tls.Certificate, error) { + // check for a stored cert + certData := m.getCache("cert", name) + if len(certData) == 0 { + return nil, errors.New("autocert: no existing certificate") + } + + privBlock, pubData := pem.Decode(certData) + if len(pubData) == 0 { + return nil, errors.New("autocert: no public key data (cert/issuer)") + } + + // decode pub chain + var pubDER [][]byte + var pub []byte + for len(pubData) > 0 { + var b *pem.Block + b, pubData = pem.Decode(pubData) + if b == nil { + break + } + pubDER = append(pubDER, b.Bytes) + pub = append(pub, b.Bytes...) + } + if len(pubData) > 0 { + return nil, errors.New("autocert: leftover data in file - possibly corrupt") + } + + certs, err := x509.ParseCertificates(pub) + if err != nil { + return nil, fmt.Errorf("autocert: bad certificate: %v", err) + } + + leaf := certs[0] + + // add any intermediate certs if present + var intermediates *x509.CertPool + if len(certs) > 1 { + intermediates = x509.NewCertPool() + for i := 1; i < len(certs); i++ { + intermediates.AddCert(certs[i]) + } + } + + // add a root certificate if present + var roots *x509.CertPool + if m.RootCert != "" { + block, rest := pem.Decode([]byte(m.RootCert)) + for block != nil { + rootCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.New("autocert: error parsing root certificate") + } + if roots == nil { + roots = x509.NewCertPool() + } + roots.AddCert(rootCert) + block, rest = pem.Decode(rest) + } + } + + opts := x509.VerifyOptions{ + DNSName: name, + Intermediates: intermediates, + Roots: roots, + } + + if _, err := leaf.Verify(opts); err != nil { + return nil, fmt.Errorf("autocert: unable to verify: %v", err) + } + + privKey, err := x509.ParseECPrivateKey(privBlock.Bytes) + if err != nil { + return nil, errors.New("autocert: invalid private key") + } + + return &tls.Certificate{ + Certificate: pubDER, + PrivateKey: privKey, + Leaf: leaf, + }, nil +} + +func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) { + // attempt to load an existing account key + var privKey *ecdsa.PrivateKey + if keyData := m.getCache("account"); len(keyData) > 0 { + block, _ := pem.Decode(keyData) + x509Encoded := block.Bytes + privKey, _ = x509.ParseECPrivateKey(x509Encoded) + } + + // otherwise generate a new one + if privKey == nil { + var err error + privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("autocert: error generating new account key: %v", err) + } + + x509Encoded, _ := x509.MarshalECPrivateKey(privKey) + pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded}) + + m.putCache(pemEncoded, "account") + } + + // create a new client if one doesn't exist + if m.client.Directory().URL == "" { + var err error + m.client, err = NewClient(m.getDirectoryURL(), m.Options...) + if err != nil { + return nil, err + } + } + + // create/fetch acme account + account, err := m.client.NewAccount(privKey, false, true) + if err != nil { + return nil, fmt.Errorf("autocert: error creating/fetching account: %v", err) + } + + // start a new order process + order, err := m.client.NewOrderDomains(account, domainName) + if err != nil { + return nil, fmt.Errorf("autocert: error creating new order for domain %s: %v", domainName, err) + } + + // loop through each of the provided authorization Urls + for _, authURL := range order.Authorizations { + auth, err := m.client.FetchAuthorization(account, authURL) + if err != nil { + return nil, fmt.Errorf("autocert: error fetching authorization Url %q: %v", authURL, err) + } + + if auth.Status == "valid" { + continue + } + + chal, ok := auth.ChallengeMap[ChallengeTypeHTTP01] + if !ok { + return nil, fmt.Errorf("autocert: unable to find http-01 challenge for auth %s, Url: %s", auth.Identifier.Value, authURL) + } + + m.tokensLock.Lock() + if m.tokens == nil { + m.tokens = map[string][]byte{} + } + m.tokens[chal.Token] = []byte(chal.KeyAuthorization) + m.tokensLock.Unlock() + + if m.PreUpdateChallengeHook != nil { + m.PreUpdateChallengeHook(account, chal) + } + + chal, err = m.client.UpdateChallenge(account, chal) + if err != nil { + return nil, fmt.Errorf("autocert: error updating authorization %s challenge (Url: %s) : %v", auth.Identifier.Value, authURL, err) + } + + m.tokensLock.Lock() + delete(m.tokens, chal.Token) + m.tokensLock.Unlock() + } + + // generate private key for cert + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("autocert: error generating certificate key for %s: %v", domainName, err) + } + certKeyEnc, err := x509.MarshalECPrivateKey(certKey) + if err != nil { + return nil, fmt.Errorf("autocert: error encoding certificate key for %s: %v", domainName, err) + } + certKeyPem := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: certKeyEnc, + }) + + // create the new csr template + tpl := &x509.CertificateRequest{ + SignatureAlgorithm: x509.ECDSAWithSHA256, + PublicKeyAlgorithm: x509.ECDSA, + PublicKey: certKey.Public(), + Subject: pkix.Name{CommonName: domainName}, + DNSNames: []string{domainName}, + } + csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey) + if err != nil { + return nil, fmt.Errorf("autocert: error creating certificate request for %s: %v", domainName, err) + } + csr, err := x509.ParseCertificateRequest(csrDer) + if err != nil { + return nil, fmt.Errorf("autocert: error parsing certificate request for %s: %v", domainName, err) + } + + // finalize the order with the acme server given a csr + order, err = m.client.FinalizeOrder(account, order, csr) + if err != nil { + return nil, fmt.Errorf("autocert: error finalizing order for %s: %v", domainName, err) + } + + // fetch the certificate chain from the finalized order provided by the acme server + certs, err := m.client.FetchCertificates(account, order.Certificate) + if err != nil { + return nil, fmt.Errorf("autocert: error fetching order certificates for %s: %v", domainName, err) + } + + certPem := certKeyPem + // var certDer [][]byte + for _, c := range certs { + b := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: c.Raw, + }) + certPem = append(certPem, b...) + // certDer = append(certDer, c.Raw) + } + m.putCache(certPem, "cert", domainName) + + return m.getExistingCert(domainName) +} diff --git a/vendor/github.com/eggsampler/acme/v3/certificate.go b/vendor/github.com/eggsampler/acme/v3/certificate.go new file mode 100644 index 0000000000..0791f67f48 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/certificate.go @@ -0,0 +1,106 @@ +package acme + +import ( + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "net/http" +) + +func (c Client) decodeCertificateChain(body []byte, resp *http.Response, account Account) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for { + var p *pem.Block + p, body = pem.Decode(body) + if p == nil { + break + } + cert, err := x509.ParseCertificate(p.Bytes) + if err != nil { + return certs, fmt.Errorf("acme: error parsing certificate: %v", err) + } + certs = append(certs, cert) + } + + up := fetchLink(resp, "up") + if up != "" { + upCerts, err := c.FetchCertificates(account, up) + if err != nil { + return certs, fmt.Errorf("acme: error fetching up cert: %v", err) + } + if len(upCerts) != 0 { + certs = append(certs, upCerts...) + } + } + + return certs, nil +} + +// FetchCertificates downloads a certificate chain from a url given in an order certificate. +func (c Client) FetchCertificates(account Account, certificateURL string) ([]*x509.Certificate, error) { + resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK}) + if err != nil { + return nil, err + } + + return c.decodeCertificateChain(body, resp, account) +} + +// FetchAllCertificates downloads a certificate chain from a url given in an order certificate, as well as any alternate certificates if provided. +// Returns a mapping of certificate urls to the certificate chain. +func (c Client) FetchAllCertificates(account Account, certificateURL string) (map[string][]*x509.Certificate, error) { + resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK}) + if err != nil { + return nil, err + } + + certChain, err := c.decodeCertificateChain(body, resp, account) + if err != nil { + return nil, err + } + + certs := map[string][]*x509.Certificate{ + certificateURL: certChain, + } + + alternates := fetchLinks(resp, "alternate") + + for _, altURL := range alternates { + altResp, altBody, err := c.postRaw(0, altURL, account.URL, account.PrivateKey, "", []int{http.StatusOK}) + if err != nil { + return certs, fmt.Errorf("acme: error fetching alt cert chain at %q - %v", altURL, err) + } + altCertChain, err := c.decodeCertificateChain(altBody, altResp, account) + if err != nil { + return certs, fmt.Errorf("acme: error decoding alt cert chain at %q - %v", altURL, err) + } + certs[altURL] = altCertChain + } + + return certs, nil + +} + +// RevokeCertificate revokes a given certificate given the certificate key or account key, and a reason. +func (c Client) RevokeCertificate(account Account, cert *x509.Certificate, key crypto.Signer, reason int) error { + revokeReq := struct { + Certificate string `json:"certificate"` + Reason int `json:"reason"` + }{ + Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw), + Reason: reason, + } + + kid := "" + if key == account.PrivateKey { + kid = account.URL + } + + if _, err := c.post(c.dir.RevokeCert, kid, key, revokeReq, nil, http.StatusOK); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/eggsampler/acme/v3/challenge.go b/vendor/github.com/eggsampler/acme/v3/challenge.go new file mode 100644 index 0000000000..23a1389341 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/challenge.go @@ -0,0 +1,102 @@ +package acme + +import ( + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/http" + "time" +) + +// EncodeDNS01KeyAuthorization encodes a key authorization and provides a value to be put in the TXT record for the _acme-challenge DNS entry. +func EncodeDNS01KeyAuthorization(keyAuth string) string { + h := sha256.Sum256([]byte(keyAuth)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} + +// Helper function to determine whether a challenge is "finished" by its status. +func checkUpdatedChallengeStatus(challenge Challenge) (bool, error) { + switch challenge.Status { + case "pending": + // Challenge objects are created in the "pending" state. + // TODO: https://github.com/letsencrypt/boulder/issues/3346 + // return true, errors.New("acme: unexpected 'pending' challenge state") + return false, nil + + case "processing": + // They transition to the "processing" state when the client responds to the + // challenge and the server begins attempting to validate that the client has completed the challenge. + return false, nil + + case "valid": + // If validation is successful, the challenge moves to the "valid" state + return true, nil + + case "invalid": + // if there is an error, the challenge moves to the "invalid" state. + if challenge.Error.Type != "" { + return true, challenge.Error + } + return true, errors.New("acme: challenge is invalid, no error provided") + + default: + return true, fmt.Errorf("acme: unknown challenge status: %s", challenge.Status) + } +} + +// UpdateChallenge responds to a challenge to indicate to the server to complete the challenge. +func (c Client) UpdateChallenge(account Account, challenge Challenge) (Challenge, error) { + resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, struct{}{}, &challenge, http.StatusOK) + if err != nil { + return challenge, err + } + + if loc := resp.Header.Get("Location"); loc != "" { + challenge.URL = loc + } + challenge.AuthorizationURL = fetchLink(resp, "up") + + if finished, err := checkUpdatedChallengeStatus(challenge); finished { + return challenge, err + } + + pollInterval, pollTimeout := c.getPollingDurations() + end := time.Now().Add(pollTimeout) + for { + if time.Now().After(end) { + return challenge, errors.New("acme: challenge update timeout") + } + time.Sleep(pollInterval) + + resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK) + if err != nil { + // i don't think it's worth exiting the loop on this error + // it could just be connectivity issue that's resolved before the timeout duration + continue + } + + if loc := resp.Header.Get("Location"); loc != "" { + challenge.URL = loc + } + challenge.AuthorizationURL = fetchLink(resp, "up") + + if finished, err := checkUpdatedChallengeStatus(challenge); finished { + return challenge, err + } + } +} + +// FetchChallenge fetches an existing challenge from the given url. +func (c Client) FetchChallenge(account Account, challengeURL string) (Challenge, error) { + challenge := Challenge{} + resp, err := c.post(challengeURL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK) + if err != nil { + return challenge, err + } + + challenge.URL = resp.Header.Get("Location") + challenge.AuthorizationURL = fetchLink(resp, "up") + + return challenge, nil +} diff --git a/vendor/github.com/eggsampler/acme/v3/docker-compose.boulder-temp.yml b/vendor/github.com/eggsampler/acme/v3/docker-compose.boulder-temp.yml new file mode 100644 index 0000000000..8455b23be3 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/docker-compose.boulder-temp.yml @@ -0,0 +1,5 @@ +version: '3' +services: + boulder: + ports: + - "8055:8055" \ No newline at end of file diff --git a/vendor/github.com/eggsampler/acme/v3/jws.go b/vendor/github.com/eggsampler/acme/v3/jws.go new file mode 100644 index 0000000000..b38828d859 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/jws.go @@ -0,0 +1,257 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acme + +import ( + "crypto" + "crypto/ecdsa" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // need for EC keys + "encoding/asn1" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" +) + +// KeyID is the account key identity provided by a CA during registration. +type KeyID string + +// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. +// See jwsEncodeJSON for details. +const noKeyID = KeyID("") + +// noPayload indicates jwsEncodeJSON will encode zero-length octet string +// in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make +// authenticated GET requests via POSTing with an empty payload. +// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details. +const noPayload = "" + +// noNonce indicates that the nonce should be omitted from the protected header. +// See jwsEncodeJSON for details. +const noNonce = "" + +// jsonWebSignature can be easily serialized into a JWS following +// https://tools.ietf.org/html/rfc7515#section-3.2. +type jsonWebSignature struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` +} + +// jwsEncodeJSON signs claimset using provided key and a nonce. +// The result is serialized in JSON format containing either kid or jwk +// fields based on the provided KeyID value. +// +// The claimset is marshalled using json.Marshal unless it is a string. +// In which case it is inserted directly into the message. +// +// If kid is non-empty, its quoted value is inserted in the protected header +// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted +// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive. +// +// If nonce is non-empty, its quoted value is inserted in the protected header. +// +// See https://tools.ietf.org/html/rfc7515#section-7. +func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) { + if key == nil { + return nil, errors.New("nil key") + } + alg, sha := jwsHasher(key.Public()) + if alg == "" || !sha.Available() { + return nil, ErrUnsupportedKey + } + headers := struct { + Alg string `json:"alg"` + KID string `json:"kid,omitempty"` + JWK json.RawMessage `json:"jwk,omitempty"` + Nonce string `json:"nonce,omitempty"` + URL string `json:"url"` + }{ + Alg: alg, + Nonce: nonce, + URL: url, + } + switch kid { + case noKeyID: + jwk, err := jwkEncode(key.Public()) + if err != nil { + return nil, err + } + headers.JWK = json.RawMessage(jwk) + default: + headers.KID = string(kid) + } + phJSON, err := json.Marshal(headers) + if err != nil { + return nil, err + } + phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON)) + var payload string + if val, ok := claimset.(string); ok { + payload = val + } else { + cs, err := json.Marshal(claimset) + if err != nil { + return nil, err + } + payload = base64.RawURLEncoding.EncodeToString(cs) + } + hash := sha.New() + hash.Write([]byte(phead + "." + payload)) + sig, err := jwsSign(key, sha, hash.Sum(nil)) + if err != nil { + return nil, err + } + enc := jsonWebSignature{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + return json.Marshal(&enc) +} + +// jwsWithMAC creates and signs a JWS using the given key and the HS256 +// algorithm. kid and url are included in the protected header. rawPayload +// should not be base64-URL-encoded. +func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) { + if len(key) == 0 { + return nil, errors.New("acme: cannot sign JWS with an empty MAC key") + } + header := struct { + Algorithm string `json:"alg"` + KID string `json:"kid"` + URL string `json:"url,omitempty"` + }{ + // Only HMAC-SHA256 is supported. + Algorithm: "HS256", + KID: kid, + URL: url, + } + rawProtected, err := json.Marshal(header) + if err != nil { + return nil, err + } + protected := base64.RawURLEncoding.EncodeToString(rawProtected) + payload := base64.RawURLEncoding.EncodeToString(rawPayload) + + h := hmac.New(sha256.New, key) + if _, err := h.Write([]byte(protected + "." + payload)); err != nil { + return nil, err + } + mac := h.Sum(nil) + + return &jsonWebSignature{ + Protected: protected, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(mac), + }, nil +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", ErrUnsupportedKey +} + +// jwsSign signs the digest using the given key. +// The hash is unused for ECDSA keys. +func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { + switch pub := key.Public().(type) { + case *rsa.PublicKey: + return key.Sign(rand.Reader, digest, hash) + case *ecdsa.PublicKey: + sigASN1, err := key.Sign(rand.Reader, digest, hash) + if err != nil { + return nil, err + } + + var rs struct{ R, S *big.Int } + if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil { + return nil, err + } + + rb, sb := rs.R.Bytes(), rs.S.Bytes() + size := pub.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return nil, ErrUnsupportedKey +} + +// jwsHasher indicates suitable JWS algorithm name and a hash function +// to use for signing a digest with the provided key. +// It returns ("", 0) if the key is not supported. +func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) { + switch pub := pub.(type) { + case *rsa.PublicKey: + return "RS256", crypto.SHA256 + case *ecdsa.PublicKey: + switch pub.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-521": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + +// JWKThumbprint creates a JWK thumbprint out of pub +// as specified in https://tools.ietf.org/html/rfc7638. +func JWKThumbprint(pub crypto.PublicKey) (string, error) { + jwk, err := jwkEncode(pub) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(jwk)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} diff --git a/vendor/github.com/eggsampler/acme/v3/nonce.go b/vendor/github.com/eggsampler/acme/v3/nonce.go new file mode 100644 index 0000000000..2ef9aca733 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/nonce.go @@ -0,0 +1,45 @@ +package acme + +import ( + "sync" +) + +// Simple thread-safe stack impl +type nonceStack struct { + lock sync.Mutex + stack []string +} + +// Pushes a nonce to the stack. +// Doesn't push empty nonces, or if there's more than 100 nonces on the stack +func (ns *nonceStack) push(v string) { + if v == "" { + return + } + + ns.lock.Lock() + defer ns.lock.Unlock() + + if len(ns.stack) > 100 { + return + } + + ns.stack = append(ns.stack, v) +} + +// Pops a nonce from the stack. +// Returns empty string if there are no nonces +func (ns *nonceStack) pop() string { + ns.lock.Lock() + defer ns.lock.Unlock() + + n := len(ns.stack) + if n == 0 { + return "" + } + + v := ns.stack[n-1] + ns.stack = ns.stack[:n-1] + + return v +} diff --git a/vendor/github.com/eggsampler/acme/v3/options.go b/vendor/github.com/eggsampler/acme/v3/options.go new file mode 100644 index 0000000000..c1a7a4b692 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/options.go @@ -0,0 +1,173 @@ +package acme + +import ( + "crypto" + "crypto/hmac" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +// OptionFunc function prototype for passing options to NewClient +type OptionFunc func(client *Client) error + +// WithHTTPTimeout sets a timeout on the http client used by the Client +func WithHTTPTimeout(duration time.Duration) OptionFunc { + return func(client *Client) error { + client.httpClient.Timeout = duration + return nil + } +} + +// WithInsecureSkipVerify sets InsecureSkipVerify on the http client transport tls client config used by the Client +func WithInsecureSkipVerify() OptionFunc { + return func(client *Client) error { + client.httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + return nil + } +} + +// WithUserAgentSuffix appends a user agent suffix for http requests to acme resources +func WithUserAgentSuffix(userAgentSuffix string) OptionFunc { + return func(client *Client) error { + client.userAgentSuffix = userAgentSuffix + return nil + } +} + +// WithAcceptLanguage sets an Accept-Language header on http requests +func WithAcceptLanguage(acceptLanguage string) OptionFunc { + return func(client *Client) error { + client.acceptLanguage = acceptLanguage + return nil + } +} + +// WithRetryCount sets the number of times the acme client retries when receiving an api error (eg, nonce failures, etc). +// Default: 5 +func WithRetryCount(retryCount int) OptionFunc { + return func(client *Client) error { + if retryCount < 1 { + return errors.New("retryCount must be > 0") + } + client.retryCount = retryCount + return nil + } +} + +// WithHTTPClient Allows setting a custom http client for acme connections +func WithHTTPClient(httpClient *http.Client) OptionFunc { + return func(client *Client) error { + if httpClient == nil { + return errors.New("client must not be nil") + } + client.httpClient = httpClient + return nil + } +} + +// WithRootCerts sets the httpclient transport to use a given certpool for root certs +func WithRootCerts(pool *x509.CertPool) OptionFunc { + return func(client *Client) error { + client.httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + }, + } + return nil + } +} + +// NewAccountOptionFunc function prototype for passing options to NewClient +type NewAccountOptionFunc func(crypto.Signer, *Account, *NewAccountRequest, Client) error + +// NewAcctOptOnlyReturnExisting sets the new client request to only return existing accounts +func NewAcctOptOnlyReturnExisting() NewAccountOptionFunc { + return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { + request.OnlyReturnExisting = true + return nil + } +} + +// NewAcctOptAgreeTOS sets the new account request as agreeing to the terms of service +func NewAcctOptAgreeTOS() NewAccountOptionFunc { + return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { + request.TermsOfServiceAgreed = true + return nil + } +} + +// NewAcctOptWithContacts adds contacts to a new account request +func NewAcctOptWithContacts(contacts ...string) NewAccountOptionFunc { + return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { + request.Contact = contacts + return nil + } +} + +// NewAcctOptExternalAccountBinding adds an external account binding to the new account request +// Code adopted from jwsEncodeJSON +func NewAcctOptExternalAccountBinding(binding ExternalAccountBinding) NewAccountOptionFunc { + return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { + if binding.KeyIdentifier == "" { + return errors.New("acme: NewAcctOptExternalAccountBinding has no KeyIdentifier set") + } + if binding.MacKey == "" { + return errors.New("acme: NewAcctOptExternalAccountBinding has no MacKey set") + } + if binding.Algorithm == "" { + return errors.New("acme: NewAcctOptExternalAccountBinding has no Algorithm set") + } + if binding.HashFunc == 0 { + return errors.New("acme: NewAcctOptExternalAccountBinding has no HashFunc set") + } + + jwk, err := jwkEncode(privateKey.Public()) + if err != nil { + return fmt.Errorf("acme: external account binding error encoding public key: %v", err) + } + payload := base64.RawURLEncoding.EncodeToString([]byte(jwk)) + + phead := fmt.Sprintf(`{"alg":%q,"kid":%q,"url":%q}`, + binding.Algorithm, binding.KeyIdentifier, client.Directory().NewAccount) + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + + decodedAccountMac, err := base64.RawURLEncoding.DecodeString(binding.MacKey) + if err != nil { + return fmt.Errorf("acme: external account binding error decoding mac key: %v", err) + } + macHash := hmac.New(binding.HashFunc.New, decodedAccountMac) + + if _, err := macHash.Write([]byte(phead + "." + payload)); err != nil { + return err + } + + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(macHash.Sum(nil)), + } + + jwsEab, err := json.Marshal(&enc) + if err != nil { + return fmt.Errorf("acme: external account binding error marshalling struct: %v", err) + } + + request.ExternalAccountBinding = jwsEab + account.ExternalAccountBinding = binding + return nil + } +} diff --git a/vendor/github.com/eggsampler/acme/v3/order.go b/vendor/github.com/eggsampler/acme/v3/order.go new file mode 100644 index 0000000000..364abbf509 --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/order.go @@ -0,0 +1,231 @@ +package acme + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "net/http" + "time" +) + +type OrderExtension struct { + Profile string +} + +// NewOrder initiates a new order for a new certificate. This method does not use ACME Renewal Info. +func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) { + return c.ReplacementOrder(account, nil, identifiers) +} + +// NewOrderDomains takes a list of domain dns identifiers for a new certificate. Essentially a helper function. +func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) { + var identifiers []Identifier + for _, d := range domains { + identifiers = append(identifiers, Identifier{Type: "dns", Value: d}) + } + return c.ReplacementOrder(account, nil, identifiers) +} + +// NewOrderExtension takes a struct providing any extensions onto the order +func (c Client) NewOrderExtension(account Account, identifiers []Identifier, ext OrderExtension) (Order, error) { + return c.ReplacementOrderExtension(account, nil, identifiers, ext) +} + +// ReplacementOrder takes an existing *x509.Certificate and initiates a new +// order for a new certificate, but with the order being marked as a +// replacement. Replacement orders which are valid replacements are (currently) +// exempt from Let's Encrypt NewOrder rate limits, but may not be exempt from +// other ACME CAs ACME Renewal Info implementations. At least one identifier +// must match the list of identifiers from the parent order to be considered as +// a valid replacement order. +// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 +func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, identifiers []Identifier) (Order, error) { + return c.ReplacementOrderExtension(account, oldCert, identifiers, OrderExtension{}) +} + +// ReplacementOrderExtension takes a struct providing any extensions onto the order +func (c Client) ReplacementOrderExtension(account Account, oldCert *x509.Certificate, identifiers []Identifier, ext OrderExtension) (Order, error) { + // If an old cert being replaced is present and the acme directory doesn't list a RenewalInfo endpoint, + // throw an error. This endpoint being present indicates support for ARI. + if oldCert != nil && c.dir.RenewalInfo == "" { + return Order{}, ErrRenewalInfoNotSupported + } + + // optional fields are listed as 'omitempty' so the json encoder doesn't + // include those keys if their values are not provided. + newOrderReq := struct { + Identifiers []Identifier `json:"identifiers"` + Replaces string `json:"replaces,omitempty"` + Profile string `json:"Profile,omitempty"` + }{ + Identifiers: identifiers, + } + + newOrderResp := Order{} + + if ext.Profile != "" { + _, ok := c.Directory().Meta.Profiles[ext.Profile] + if !ok { + return Order{}, fmt.Errorf("requested Profile not advertised by directory: %v", ext.Profile) + } + newOrderReq.Profile = ext.Profile + } + + // If present, add the ari cert ID from the original/old certificate + if oldCert != nil { + replacesCertID, err := GenerateARICertID(oldCert) + if err != nil { + return Order{}, fmt.Errorf("acme: error generating replacement certificate id: %v", err) + } + + newOrderReq.Replaces = replacesCertID + newOrderResp.Replaces = replacesCertID // server does not appear to set this currently? + } + + // Submit the order + resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated) + if err != nil { + return newOrderResp, err + } + defer resp.Body.Close() + + newOrderResp.URL = resp.Header.Get("Location") + return newOrderResp, nil +} + +// FetchOrder fetches an existing order given an order url. +func (c Client) FetchOrder(account Account, orderURL string) (Order, error) { + orderResp := Order{ + URL: orderURL, // boulder response doesn't seem to contain location header for this request + } + _, err := c.post(orderURL, account.URL, account.PrivateKey, "", &orderResp, http.StatusOK) + + return orderResp, err +} + +// Helper function to determine whether an order is "finished" by its status. +func checkFinalizedOrderStatus(order Order) (bool, error) { + switch order.Status { + case "invalid": + // "invalid": The certificate will not be issued. Consider this + // order process abandoned. + if order.Error.Type != "" { + return true, order.Error + } + return true, errors.New("acme: finalized order is invalid, no error provided") + + case "pending": + // "pending": The server does not believe that the client has + // fulfilled the requirements. Check the "authorizations" array for + // entries that are still pending. + return true, errors.New("acme: authorizations not fulfilled") + + case "ready": + // "ready": The server agrees that the requirements have been + // fulfilled, and is awaiting finalization. Submit a finalization + // request. + return true, errors.New("acme: unexpected 'ready' state") + + case "processing": + // "processing": The certificate is being issued. Send a GET request + // after the time given in the "Retry-After" header field of the + // response, if any. + return false, nil + + case "valid": + // "valid": The server has issued the certificate and provisioned its + // URL to the "certificate" field of the order. Download the + // certificate. + return true, nil + + default: + return true, fmt.Errorf("acme: unknown order status: %s", order.Status) + } +} + +// FinalizeOrder indicates to the acme server that the client considers an order complete and "finalizes" it. +// If the server believes the authorizations have been filled successfully, a certificate should then be available. +// This function assumes that the order status is "ready". +func (c Client) FinalizeOrder(account Account, order Order, csr *x509.CertificateRequest) (Order, error) { + finaliseReq := struct { + Csr string `json:"csr"` + }{ + Csr: base64.RawURLEncoding.EncodeToString(csr.Raw), + } + + resp, err := c.post(order.Finalize, account.URL, account.PrivateKey, finaliseReq, &order, http.StatusOK) + if err != nil { + return order, err + } + + order.URL = resp.Header.Get("Location") + + updateOrder := func(resp *http.Response) (bool, error) { + if finished, err := checkFinalizedOrderStatus(order); finished { + return true, err + } + + retryAfter, err := parseRetryAfter(resp.Header.Get("Retry-After")) + if err != nil { + return false, fmt.Errorf("acme: error parsing retry-after header: %v", err) + } + order.RetryAfter = retryAfter + + return false, nil + } + + if finished, err := updateOrder(resp); finished || err != nil { + return order, err + } + + fetchOrder := func() (bool, error) { + resp, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK) + if err != nil { + return false, nil + } + + return updateOrder(resp) + } + + if !c.IgnoreRetryAfter && !order.RetryAfter.IsZero() { + _, pollTimeout := c.getPollingDurations() + end := time.Now().Add(pollTimeout) + + for { + if time.Now().After(end) { + return order, errors.New("acme: finalized order timeout") + } + + diff := time.Until(order.RetryAfter) + _, pollTimeout := c.getPollingDurations() + if diff > pollTimeout { + return order, fmt.Errorf("acme: Retry-After (%v) longer than poll timeout (%v)", diff, c.PollTimeout) + } + if diff > 0 { + time.Sleep(diff) + } + + if finished, err := fetchOrder(); finished || err != nil { + return order, err + } + } + } + + if !c.IgnoreRetryAfter { + pollInterval, pollTimeout := c.getPollingDurations() + end := time.Now().Add(pollTimeout) + for { + if time.Now().After(end) { + return order, errors.New("acme: finalized order timeout") + } + time.Sleep(pollInterval) + + if finished, err := fetchOrder(); finished || err != nil { + return order, err + } + } + } + + return order, err +} diff --git a/vendor/github.com/eggsampler/acme/v3/problem.go b/vendor/github.com/eggsampler/acme/v3/problem.go new file mode 100644 index 0000000000..4c3ae0d54b --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/problem.go @@ -0,0 +1,65 @@ +package acme + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// Problem document as defined in, +// https://tools.ietf.org/html/rfc7807 + +// Problem represents an error returned by an acme server. +type Problem struct { + Type string `json:"type"` + Detail string `json:"detail,omitempty"` + Status int `json:"status,omitempty"` + Instance string `json:"instance,omitempty"` + SubProblems []SubProblem `json:"subproblems,omitempty"` +} + +type SubProblem struct { + Type string `json:"type"` + Detail string `json:"detail"` + Identifier Identifier `json:"identifier"` +} + +// Returns a human readable error string. +func (err Problem) Error() string { + s := fmt.Sprintf("acme: error code %d %q: %s", err.Status, err.Type, err.Detail) + if len(err.SubProblems) > 0 { + for _, v := range err.SubProblems { + s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail) + } + } + if err.Instance != "" { + s += ", url: " + err.Instance + } + return s +} + +// Helper function to determine if a response contains an expected status code, or otherwise an error object. +func checkError(resp *http.Response, expectedStatuses ...int) error { + for _, statusCode := range expectedStatuses { + if resp.StatusCode == statusCode { + return nil + } + } + + if resp.StatusCode < 400 || resp.StatusCode >= 600 { + return fmt.Errorf("acme: expected status codes: %d, got: %d %s", expectedStatuses, resp.StatusCode, resp.Status) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("acme: error reading error body: %v", err) + } + + acmeError := Problem{} + if err := json.Unmarshal(body, &acmeError); err != nil { + return fmt.Errorf("acme: parsing error body: %v - %s", err, string(body)) + } + + return acmeError +} diff --git a/vendor/github.com/eggsampler/acme/v3/types.go b/vendor/github.com/eggsampler/acme/v3/types.go new file mode 100644 index 0000000000..d02982983b --- /dev/null +++ b/vendor/github.com/eggsampler/acme/v3/types.go @@ -0,0 +1,236 @@ +package acme + +import ( + "crypto" + "encoding/json" + "errors" + "net/http" + "time" +) + +var ( + // ErrUnsupportedKey is returned when an unsupported key type is encountered. + ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported") + + // ErrRenewalInfoNotSupported is returned by Client.GetRenewalInfo if the + // renewal info entry isn't present on the acme directory (ie, it's not + // supported by the acme server) + ErrRenewalInfoNotSupported = errors.New("renewal information endpoint not supported") +) + +// Different possible challenge types provided by an ACME server. +// See https://tools.ietf.org/html/rfc8555#section-9.7.8 +const ( + ChallengeTypeDNS01 = "dns-01" + ChallengeTypeDNSAccount01 = "dns-account-01" + ChallengeTypeHTTP01 = "http-01" + ChallengeTypeTLSALPN01 = "tls-alpn-01" + + // ChallengeTypeTLSSNI01 is deprecated and should not be used. + // See: https://community.letsencrypt.org/t/important-what-you-need-to-know-about-tls-sni-validation-issues/50811 + ChallengeTypeTLSSNI01 = "tls-sni-01" +) + +// Constants used for certificate revocation, used for RevokeCertificate +// See https://tools.ietf.org/html/rfc5280#section-5.3.1 +const ( + ReasonUnspecified = iota // 0 + ReasonKeyCompromise // 1 + ReasonCaCompromise // 2 + ReasonAffiliationChanged // 3 + ReasonSuperseded // 4 + ReasonCessationOfOperation // 5 + ReasonCertificateHold // 6 + _ // 7 - Unused + ReasonRemoveFromCRL // 8 + ReasonPrivilegeWithdrawn // 9 + ReasonAaCompromise // 10 +) + +// Directory object as returned from the client's directory url upon creation of client. +// See https://tools.ietf.org/html/rfc8555#section-7.1.1 +type Directory struct { + NewNonce string `json:"newNonce"` // url to new nonce endpoint + NewAccount string `json:"newAccount"` // url to new account endpoint + NewOrder string `json:"newOrder"` // url to new order endpoint + NewAuthz string `json:"newAuthz"` // url to new authz endpoint + RevokeCert string `json:"revokeCert"` // url to revoke cert endpoint + KeyChange string `json:"keyChange"` // url to key change endpoint + + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03 + RenewalInfo string `json:"renewalInfo"` // url to renewal info endpoint + + // meta object containing directory metadata + Meta struct { + TermsOfService string `json:"termsOfService"` + Website string `json:"website"` + CaaIdentities []string `json:"caaIdentities"` + ExternalAccountRequired bool `json:"externalAccountRequired"` + Profiles map[string]string `json:"profiles"` + } `json:"meta"` + + // Directory url provided when creating a new acme client. + URL string `json:"-"` +} + +// Client structure to interact with an ACME server. +// This is typically how most, if not all, of the communication between the client and server occurs. +type Client struct { + httpClient *http.Client + nonces *nonceStack + dir Directory + userAgentSuffix string + acceptLanguage string + retryCount int + + // The amount of total time the Client will wait at most for a challenge to be updated or a certificate to be issued. + // Default 30 seconds if duration is not set or if set to 0. + PollTimeout time.Duration + + // The time between checking if a challenge has been updated or a certificate has been issued. + // Default 0.5 seconds if duration is not set or if set to 0. + PollInterval time.Duration + + // IgnorePolling does not use any simple polling in order finalisation + IgnorePolling bool + + // IgnoreRetryAfter does not use the retry-after header in order finalisation + IgnoreRetryAfter bool +} + +// Account structure representing fields in an account object. +// See https://tools.ietf.org/html/rfc8555#section-7.1.2 +// See also https://tools.ietf.org/html/rfc8555#section-9.7.1 +type Account struct { + Status string `json:"status"` + Contact []string `json:"contact"` + Orders string `json:"orders"` + + // Provided by the Location http header when creating a new account or fetching an existing account. + URL string `json:"-"` + + // The private key used to create or fetch the account. + // Not fetched from server. + PrivateKey crypto.Signer `json:"-"` + + // Thumbprint is the SHA-256 digest JWK_Thumbprint of the account key. + // See https://tools.ietf.org/html/rfc8555#section-8.1 + Thumbprint string `json:"-"` + + // ExternalAccountBinding is populated when using the NewAcctOptExternalAccountBinding option for NewAccountOption + // and is otherwise empty. Not populated when account is fetched or created otherwise. + ExternalAccountBinding ExternalAccountBinding `json:"-"` +} + +// ExternalAccountBinding holds the key identifier and mac key provided for use in servers that support/require +// external account binding. +// The MacKey is a base64url-encoded string. +// Algorithm is a "MAC-based algorithm" as per RFC8555. Typically this is either, +// - "HS256" for HashFunc: crypto.SHA256 +// - "HS384" for HashFunc: crypto.SHA384 +// - "HS512" for HashFunc: crypto.SHA512 +// +// However this is dependent on the acme server in question and is provided here to give more options for future compatibility. +type ExternalAccountBinding struct { + KeyIdentifier string `json:"-"` + MacKey string `json:"-"` + Algorithm string `json:"-"` + HashFunc crypto.Hash `json:"-"` +} + +// Identifier object used in order and authorization objects +// See https://tools.ietf.org/html/rfc8555#section-7.1.4 +type Identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// Order object returned when fetching or creating a new order. +// See https://tools.ietf.org/html/rfc8555#section-7.1.3 +type Order struct { + Status string `json:"status"` + Expires time.Time `json:"expires"` + Identifiers []Identifier `json:"identifiers"` + Profile string `json:"Profile,omitempty"` + NotBefore time.Time `json:"notBefore"` + NotAfter time.Time `json:"notAfter"` + Error Problem `json:"error"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` + Certificate string `json:"certificate"` + + // URL for the order object. + // Provided by the rel="Location" Link http header + URL string `json:"-"` + + // RetryAfter is the http Retry-After header from the order response + RetryAfter time.Time `json:"-"` + + // Replaces (optional, string): A string uniquely identifying a + // previously-issued certificate which this order is intended to replace. + // See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 + Replaces string `json:"replaces,omitempty"` +} + +// Authorization object returned when fetching an authorization in an order. +// See https://tools.ietf.org/html/rfc8555#section-7.1.4 +type Authorization struct { + Identifier Identifier `json:"identifier"` + Status string `json:"status"` + Expires time.Time `json:"expires"` + Challenges []Challenge `json:"challenges"` + Wildcard bool `json:"wildcard"` + + // For convenience access to the provided challenges + ChallengeMap map[string]Challenge `json:"-"` + ChallengeTypes []string `json:"-"` + + URL string `json:"-"` +} + +// Challenge object fetched in an authorization or directly from the challenge url. +// See https://tools.ietf.org/html/rfc8555#section-7.1.5 +type Challenge struct { + Type string `json:"type"` + URL string `json:"url"` + Status string `json:"status"` + Validated string `json:"validated"` + Error Problem `json:"error"` + + // Based on the challenge used + Token string `json:"token"` + KeyAuthorization string `json:"keyAuthorization"` + + // Authorization url provided by the rel="up" Link http header + AuthorizationURL string `json:"-"` +} + +// OrderList of challenge objects. +type OrderList struct { + Orders []string `json:"orders"` + + // Order list pagination, url to next orders. + // Provided by the rel="next" Link http header + Next string `json:"-"` +} + +// NewAccountRequest object used for submitting a request for a new account. +// Primarily used with NewAccountOptionFunc +type NewAccountRequest struct { + OnlyReturnExisting bool `json:"onlyReturnExisting"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + Contact []string `json:"contact,omitempty"` + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding"` +} + +// RenewalInfo stores the server-provided suggestions on when to renew +// certificates. +type RenewalInfo struct { + SuggestedWindow struct { + Start time.Time `json:"start"` + End time.Time `json:"end"` + } `json:"suggestedWindow"` + ExplanationURL string `json:"explanationURL,omitempty"` + + RetryAfter time.Time `json:"-"` +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8d44a7e82c..a7a819a1fa 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -518,6 +518,9 @@ github.com/eapache/queue # github.com/edsrzf/mmap-go v1.0.0 ## explicit github.com/edsrzf/mmap-go +# github.com/eggsampler/acme/v3 v3.6.2 +## explicit; go 1.11 +github.com/eggsampler/acme/v3 # github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a ## explicit github.com/elgatito/upnp @@ -1849,7 +1852,7 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.2.0 ## explicit; go 1.12 sigs.k8s.io/yaml -# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574 +# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed ## explicit; go 1.21 yunion.io/x/cloudmux/pkg/apis yunion.io/x/cloudmux/pkg/apis/billing @@ -1946,7 +1949,7 @@ yunion.io/x/log/hooks yunion.io/x/ovsdb/cli_util yunion.io/x/ovsdb/schema/ovn_nb yunion.io/x/ovsdb/types -# yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0 +# yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6 ## explicit; go 1.18 yunion.io/x/pkg/appctx yunion.io/x/pkg/errors diff --git a/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/cloudprovider.go b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/cloudprovider.go index 2a54217813..a18210f844 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/cloudprovider.go +++ b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/cloudprovider.go @@ -310,6 +310,7 @@ type ICloudProvider interface { GetMetrics(opts *MetricListOptions) ([]MetricValues, error) GetISSLCertificates() ([]ICloudSSLCertificate, error) + CreateISSLCertificate(opts *SSLCertificateCreateOptions) (ICloudSSLCertificate, error) GetIAiGateways() ([]IAiGateway, error) GetIAiGatewayById(id string) (IAiGateway, error) @@ -643,6 +644,10 @@ func (base *SBaseProvider) GetISSLCertificates() ([]ICloudSSLCertificate, error) return nil, errors.Wrapf(ErrNotImplemented, "GetISSLCertificates") } +func (base *SBaseProvider) CreateISSLCertificate(opts *SSLCertificateCreateOptions) (ICloudSSLCertificate, error) { + return nil, errors.Wrapf(ErrNotImplemented, "CreateISSLCertificate") +} + func (base *SBaseProvider) GetIAiGateways() ([]IAiGateway, error) { return nil, errors.Wrapf(ErrNotImplemented, "GetIAiGateways") } diff --git a/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/resources.go b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/resources.go index 48df0e2ecb..d3347e1f9d 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/resources.go +++ b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/resources.go @@ -1814,7 +1814,6 @@ type ICloudSSLCertificate interface { GetCommon() string GetCountry() string GetIssuer() string - GetExpired() bool GetEndDate() time.Time GetFingerprint() string GetCity() string diff --git a/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/ssl.go b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/ssl.go new file mode 100644 index 0000000000..5f2f225a7f --- /dev/null +++ b/vendor/yunion.io/x/cloudmux/pkg/cloudprovider/ssl.go @@ -0,0 +1,25 @@ +// 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 cloudprovider + +const ( + SSL_ISSUER_LETSENCRYPT = "Let's Encrypt" + SSL_ISSUER_ZEROSSL = "ZeroSSL" +) + +type SSLCertificateCreateOptions struct { + Certificate string + PrivateKey string +} diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/aliyun/sslcertificate.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/aliyun/sslcertificate.go index 4beb66273f..bcf441d006 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/aliyun/sslcertificate.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/aliyun/sslcertificate.go @@ -59,10 +59,6 @@ func (s *SSSLCertificate) GetIssuer() string { return s.Issuer } -func (s *SSSLCertificate) GetExpired() bool { - return s.Expired -} - func (s *SSSLCertificate) GetEndDate() time.Time { return s.EndDate } diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/storage.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/storage.go index bfeeded259..c9d92e225e 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/storage.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/storage.go @@ -709,7 +709,7 @@ func (self *SDatastore) CheckFile(ctx context.Context, remotePath string) (*SDat req, err := http.NewRequest("HEAD", url, nil) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "HEAD %s", url) } var size uint64 diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/virtualmachine.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/virtualmachine.go index 6f55dd617a..457bfe50df 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/virtualmachine.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/esxi/virtualmachine.go @@ -684,6 +684,7 @@ func (svm *SVirtualMachine) doUnregister(ctx context.Context) error { func (svm *SVirtualMachine) DeleteVM(ctx context.Context) error { err := svm.CheckFileInfo(ctx) if err != nil { + log.Errorf("CheckFileInfo %s error: %v", svm.GetName(), err) return svm.doUnregister(ctx) } return svm.doDestroy(ctx) @@ -1355,7 +1356,7 @@ func (svm *SVirtualMachine) CheckFileInfo(ctx context.Context) error { if ds.HasFile(file.Name) { _, err := ds.CheckFile(ctx, file.Name) if err != nil { - return errors.Wrap(err, "ds.CheckFile") + return errors.Wrapf(err, "CheckFile %s", file.Name) } break } diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/eip.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/eip.go index 155379ec1f..0dfb6dc98a 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/eip.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/eip.go @@ -125,9 +125,17 @@ func (addr *SAddress) GetINetworkId() string { } func (addr *SAddress) GetAssociationType() string { - if len(addr.GetAssociationExternalId()) > 0 { + if len(addr.instanceId) > 0 { return api.EIP_ASSOCIATE_TYPE_SERVER } + for _, user := range addr.Users { + if strings.HasPrefix(user, "/instances/") { + return api.EIP_ASSOCIATE_TYPE_SERVER + } + if strings.Contains(user, "/forwardingRules/") { + return api.EIP_ASSOCIATE_TYPE_LOADBALANCER + } + } return "" } diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer.go index 100f59da90..8e5ded2d48 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer.go @@ -23,6 +23,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/netutils" "yunion.io/x/pkg/utils" api "yunion.io/x/cloudmux/pkg/apis/compute" @@ -248,9 +249,11 @@ func (self *SLoadbalancer) GetIEIPs() ([]cloudprovider.ICloudEIP, error) { log.Errorf("GetAddress.GetForwardingRules %s", err) } - ret := []cloudprovider.ICloudEIP{} + ret, addrs := []cloudprovider.ICloudEIP{}, []string{} for i := range frs { - if strings.ToLower(frs[i].LoadBalancingScheme) == "external" { + ipAddr, _ := netutils.NewIPV4Addr(frs[i].IPAddress) + if netutils.IsExitAddress(ipAddr) && !utils.IsInStringArray(frs[i].IPAddress, addrs) { + addrs = append(addrs, frs[i].IPAddress) eips, err := self.region.GetEips(frs[i].IPAddress, 0, "") if err != nil { log.Errorf("GetEips %s", err) diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer_components.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer_components.go index cb4e50feeb..0b4c43569d 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer_components.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/google/loadbalancer_components.go @@ -559,15 +559,15 @@ func (self *SLoadbalancer) GetForwardingRules() ([]SForwardingRule, error) { targets := make([]string, 0) for i := range hps { - targets = append(targets, fmt.Sprintf(`(target="%s")`, hps[i].GetId())) + targets = append(targets, fmt.Sprintf(`(target = "%s")`, hps[i].SelfLink)) } for i := range hsps { - targets = append(targets, fmt.Sprintf(`(target="%s")`, hsps[i].GetId())) + targets = append(targets, fmt.Sprintf(`(target = "%s")`, hsps[i].SelfLink)) } - if strings.Contains(self.GetId(), "/backendServices/") { - targets = append(targets, fmt.Sprintf(`(backendService="%s")`, self.GetId())) + if strings.Contains(self.SelfLink, "/backendServices/") { + targets = append(targets, fmt.Sprintf(`(backendService = "%s")`, self.SelfLink)) } if len(targets) == 0 { diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/huawei/sslcertificate.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/huawei/sslcertificate.go index 6848962569..3247a92bd2 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/huawei/sslcertificate.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/huawei/sslcertificate.go @@ -9,6 +9,7 @@ import ( "net/url" "time" + "yunion.io/x/cloudmux/pkg/apis" "yunion.io/x/cloudmux/pkg/multicloud" "yunion.io/x/pkg/errors" ) @@ -66,10 +67,6 @@ func (s *SSSLCertificate) GetIssuer() string { return s.Brand } -func (s *SSSLCertificate) GetExpired() bool { - return time.Now().After(s.GetEndDate()) -} - func (s *SSSLCertificate) GetEndDate() time.Time { t, _ := time.Parse("2006-01-02 15:04:05", s.ExpireTime) return t @@ -111,11 +108,7 @@ func (s *SSSLCertificate) GetGlobalId() string { } func (s *SSSLCertificate) GetStatus() string { - if s.GetExpired() { - return "expired" - } else { - return "normal" - } + return apis.STATUS_AVAILABLE } func (s *SSSLCertificate) GetIsUpload() bool { diff --git a/vendor/yunion.io/x/cloudmux/pkg/multicloud/qcloud/certificate.go b/vendor/yunion.io/x/cloudmux/pkg/multicloud/qcloud/certificate.go index db9f379ec7..a0d966544b 100644 --- a/vendor/yunion.io/x/cloudmux/pkg/multicloud/qcloud/certificate.go +++ b/vendor/yunion.io/x/cloudmux/pkg/multicloud/qcloud/certificate.go @@ -220,14 +220,6 @@ func (self *SCertificate) GetIssuer() string { return self.ProductZhName } -func (self *SCertificate) GetExpired() bool { - if self.Status == 3 { - return true - } else { - return false - } -} - func (self *SCertificate) GetEndDate() time.Time { return self.CERTEndTime } diff --git a/vendor/yunion.io/x/pkg/utils/misc.go b/vendor/yunion.io/x/pkg/utils/misc.go index 69232c27ac..72d5cb135c 100644 --- a/vendor/yunion.io/x/pkg/utils/misc.go +++ b/vendor/yunion.io/x/pkg/utils/misc.go @@ -24,6 +24,8 @@ import ( "strconv" "strings" "time" + + "yunion.io/x/pkg/errors" ) type selectFunc func(obj interface{}) (string, error) @@ -535,6 +537,31 @@ func GetSizeKB(sizeStr, defaultSize string) (int64, error) { return bytes / 1024, nil } +func transMysqlQuery(dburl string) (string, error) { + queryPos := strings.IndexByte(dburl, '?') + if queryPos == 0 { + return "", fmt.Errorf("Missing database name") + } + var query url.Values + if queryPos > 0 { + queryStr := dburl[queryPos+1:] + if len(queryStr) > 0 { + var err error + query, err = url.ParseQuery(queryStr) + if err != nil { + return "", errors.Wrap(err, "ParseQuery") + } + } + dburl = dburl[:queryPos] + } else { + query = url.Values{} + } + query.Set("parseTime", "true") + query.Set("charset", "utf8mb4") + query.Set("interpolateParams", "true") + return dburl + "?" + query.Encode(), nil +} + func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) { if len(pySQLSrc) == 0 { err = fmt.Errorf("Empty input") @@ -543,6 +570,10 @@ func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) { dialect = "mysql" if !strings.Contains(pySQLSrc, `//`) { + pySQLSrc, err = transMysqlQuery(pySQLSrc) + if err != nil { + return + } return dialect, pySQLSrc, nil } @@ -560,26 +591,11 @@ func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) { return } user, passwd, host, port, dburl := strs[1], strs[2], strs[3], strs[4], strs[5] - queryPos := strings.IndexByte(dburl, '?') - if queryPos == 0 { - err = fmt.Errorf("Missing database name") + dburl, err = transMysqlQuery(dburl) + if err != nil { return } - var query url.Values - if queryPos > 0 { - queryStr := dburl[queryPos+1:] - if len(queryStr) > 0 { - query, err = url.ParseQuery(queryStr) - if err != nil { - return - } - } - dburl = dburl[:queryPos] - } else { - query = url.Values{} - } - query.Set("parseTime", "True") - ret = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", user, passwd, host, port, dburl, query.Encode()) + ret = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, passwd, host, port, dburl) return }