From b0f1090966614908d5dc751bded3c9a0a6c5d797 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 31 Jul 2020 02:41:49 +1000 Subject: [PATCH] Add support for TLS certificate packs (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for TLS certificate packs Updates the library to introduce support for certificate packs. API documentation: - https://api.cloudflare.com/#certificate-packs-properties - https://developers.cloudflare.com/ssl/advanced-certificate-manager * simplify response structs Co-authored-by: Patryk Szczygłowski --- certificate_packs.go | 189 ++++++++++++++++++++++ certificate_packs_test.go | 319 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 certificate_packs.go create mode 100644 certificate_packs_test.go diff --git a/certificate_packs.go b/certificate_packs.go new file mode 100644 index 00000000000..f3241562fa0 --- /dev/null +++ b/certificate_packs.go @@ -0,0 +1,189 @@ +package cloudflare + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/pkg/errors" +) + +// CertificatePackGeoRestrictions is for the structure of the geographic +// restrictions for a TLS certificate. +type CertificatePackGeoRestrictions struct { + Label string `json:"label"` +} + +// CertificatePackCertificate is the base structure of a TLS certificate that is +// contained within a certificate pack. +type CertificatePackCertificate struct { + ID int `json:"id"` + Hosts []string `json:"hosts"` + Issuer string `json:"issuer"` + Signature string `json:"signature"` + Status string `json:"status"` + BundleMethod string `json:"bundle_method"` + GeoRestrictions CertificatePackGeoRestrictions `json:"geo_restrictions"` + ZoneID string `json:"zone_id"` + UploadedOn time.Time `json:"uploaded_on"` + ModifiedOn time.Time `json:"modified_on"` + ExpiresOn time.Time `json:"expires_on"` + Priority int `json:"priority"` +} + +// CertificatePack is the overarching structure of a certificate pack response. +type CertificatePack struct { + ID string `json:"id"` + Type string `json:"type"` + Hosts []string `json:"hosts"` + Certificates []CertificatePackCertificate `json:"certificates"` + PrimaryCertificate int `json:"primary_certificate"` +} + +// CertificatePackRequest is used for requesting a new certificate. +type CertificatePackRequest struct { + Type string `json:"type"` + Hosts []string `json:"hosts"` +} + +// CertificatePackAdvancedCertificate is the structure of the advanced +// certificate pack certificate. +type CertificatePackAdvancedCertificate struct { + Type string `json:"type"` + Hosts []string `json:"hosts"` + ValidationMethod string `json:"validation_method"` + ValidityDays int `json:"validity_days"` + CertificateAuthority string `json:"certificate_authority"` + CloudflareBranding bool `json:"cloudflare_branding"` +} + +// CertificatePacksResponse is for responses where multiple certificates are +// expected. +type CertificatePacksResponse struct { + Response + Result []CertificatePack `json:"result"` +} + +// CertificatePacksDetailResponse contains a single certificate pack in the +// response. +type CertificatePacksDetailResponse struct { + Response + Result CertificatePack `json:"result"` +} + +// CertificatePacksAdvancedDetailResponse contains a single advanced certificate +// pack in the response. +type CertificatePacksAdvancedDetailResponse struct { + Response + Result CertificatePackAdvancedCertificate `json:"result"` +} + +// ListCertificatePacks returns all available TLS certificate packs for a zone. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-list-certificate-packs +func (api *API) ListCertificatePacks(zoneID string) ([]CertificatePack, error) { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs?status=all", zoneID) + res, err := api.makeRequest(http.MethodGet, uri, nil) + if err != nil { + return []CertificatePack{}, errors.Wrap(err, errMakeRequestError) + } + + var certificatePacksResponse CertificatePacksResponse + err = json.Unmarshal(res, &certificatePacksResponse) + if err != nil { + return []CertificatePack{}, errors.Wrap(err, errUnmarshalError) + } + + return certificatePacksResponse.Result, nil +} + +// CertificatePack returns a single TLS certificate pack on a zone. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-get-certificate-pack +func (api *API) CertificatePack(zoneID, certificatePackID string) (CertificatePack, error) { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs/%s", zoneID, certificatePackID) + res, err := api.makeRequest(http.MethodGet, uri, nil) + if err != nil { + return CertificatePack{}, errors.Wrap(err, errMakeRequestError) + } + + var certificatePacksDetailResponse CertificatePacksDetailResponse + err = json.Unmarshal(res, &certificatePacksDetailResponse) + if err != nil { + return CertificatePack{}, errors.Wrap(err, errUnmarshalError) + } + + return certificatePacksDetailResponse.Result, nil +} + +// CreateCertificatePack creates a new certificate pack associated with a zone. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-order-certificate-pack +func (api *API) CreateCertificatePack(zoneID string, cert CertificatePackRequest) (CertificatePack, error) { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs", zoneID) + res, err := api.makeRequest(http.MethodPost, uri, cert) + if err != nil { + return CertificatePack{}, errors.Wrap(err, errMakeRequestError) + } + + var certificatePacksDetailResponse CertificatePacksDetailResponse + err = json.Unmarshal(res, &certificatePacksDetailResponse) + if err != nil { + return CertificatePack{}, errors.Wrap(err, errUnmarshalError) + } + + return certificatePacksDetailResponse.Result, nil +} + +// DeleteCertificatePack removes a certificate pack associated with a zone. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-delete-advanced-certificate-manager-certificate-pack +func (api *API) DeleteCertificatePack(zoneID, certificateID string) error { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs/%s", zoneID, certificateID) + _, err := api.makeRequest(http.MethodDelete, uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + + return nil +} + +// CreateAdvancedCertificatePack creates a new certificate pack associated with a zone. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-order-certificate-pack +func (api *API) CreateAdvancedCertificatePack(zoneID string, cert CertificatePackAdvancedCertificate) (CertificatePackAdvancedCertificate, error) { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs/order", zoneID) + res, err := api.makeRequest(http.MethodPost, uri, cert) + if err != nil { + return CertificatePackAdvancedCertificate{}, errors.Wrap(err, errMakeRequestError) + } + + var advancedCertificatePacksDetailResponse CertificatePacksAdvancedDetailResponse + err = json.Unmarshal(res, &advancedCertificatePacksDetailResponse) + if err != nil { + return CertificatePackAdvancedCertificate{}, errors.Wrap(err, errUnmarshalError) + } + + return advancedCertificatePacksDetailResponse.Result, nil +} + +// RestartAdvancedCertificateValidation kicks off the validation process for a +// pending certificate pack. +// +// API Reference: https://api.cloudflare.com/#certificate-packs-restart-validation-for-advanced-certificate-manager-certificate-pack +func (api *API) RestartAdvancedCertificateValidation(zoneID, certificateID string) (CertificatePackAdvancedCertificate, error) { + uri := fmt.Sprintf("/zones/%s/ssl/certificate_packs/%s", zoneID, certificateID) + res, err := api.makeRequest(http.MethodPatch, uri, nil) + if err != nil { + return CertificatePackAdvancedCertificate{}, errors.Wrap(err, errMakeRequestError) + } + + var advancedCertificatePacksDetailResponse CertificatePacksAdvancedDetailResponse + err = json.Unmarshal(res, &advancedCertificatePacksDetailResponse) + if err != nil { + return CertificatePackAdvancedCertificate{}, errors.Wrap(err, errUnmarshalError) + } + + return advancedCertificatePacksDetailResponse.Result, nil +} diff --git a/certificate_packs_test.go b/certificate_packs_test.go new file mode 100644 index 00000000000..1f68a36cb6a --- /dev/null +++ b/certificate_packs_test.go @@ -0,0 +1,319 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + uploadedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ = time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + + desiredCertificatePack = CertificatePack{ + ID: "3822ff90-ea29-44df-9e55-21300bb9419b", + Type: "custom", + Hosts: []string{"example.com", "*.example.com", "www.example.com"}, + PrimaryCertificate: 12345678, + Certificates: []CertificatePackCertificate{{ + ID: 12345678, + Hosts: []string{"example.com"}, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + GeoRestrictions: CertificatePackGeoRestrictions{Label: "us"}, + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: uploadedOn, + ExpiresOn: expiresOn, + Priority: 1, + }}, + } +) + +func TestListCertificatePacks(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b", + "type": "custom", + "hosts": [ + "example.com", + "*.example.com", + "www.example.com" + ], + "certificates": [ + { + "id": 12345678, + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "geo_restrictions": { + "label": "us" + }, + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + ], + "primary_certificate": 12345678 + } + ] +} + `) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs", handler) + + want := []CertificatePack{desiredCertificatePack} + actual, err := client.ListCertificatePacks("023e105f4ecef8ad9ca31a8372d0c353") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListCertificatePack(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b", + "type": "custom", + "hosts": [ + "example.com", + "*.example.com", + "www.example.com" + ], + "certificates": [ + { + "id": 12345678, + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "geo_restrictions": { + "label": "us" + }, + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + ], + "primary_certificate": 12345678 + } +} + `) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs/3822ff90-ea29-44df-9e55-21300bb9419b", handler) + + actual, err := client.CertificatePack("023e105f4ecef8ad9ca31a8372d0c353", "3822ff90-ea29-44df-9e55-21300bb9419b") + + if assert.NoError(t, err) { + assert.Equal(t, desiredCertificatePack, actual) + } +} + +func TestCreateCertificatePack(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b", + "type": "custom", + "hosts": [ + "example.com", + "*.example.com", + "www.example.com" + ], + "certificates": [ + { + "id": 12345678, + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "geo_restrictions": { + "label": "us" + }, + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + ], + "primary_certificate": 12345678 + } +} + `) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs", handler) + + certificate := CertificatePackRequest{Type: "custom", Hosts: []string{"example.com", "*.example.com", "www.example.com"}} + actual, err := client.CreateCertificatePack("023e105f4ecef8ad9ca31a8372d0c353", certificate) + + if assert.NoError(t, err) { + assert.Equal(t, desiredCertificatePack, actual) + } +} + +func TestCreateAdvancedCertificatePack(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b", + "type": "advanced", + "hosts": [ + "example.com", + "*.example.com", + "www.example.com" + ], + "status": "initializing", + "validation_method": "txt", + "validity_days": 365, + "certificate_authority": "digicert", + "cloudflare_branding": false + } +}`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs/order", handler) + + certificate := CertificatePackAdvancedCertificate{ + Type: "advanced", + Hosts: []string{"example.com", "*.example.com", "www.example.com"}, + ValidityDays: 365, + ValidationMethod: "txt", + CertificateAuthority: "digicert", + CloudflareBranding: false, + } + + actual, err := client.CreateAdvancedCertificatePack("023e105f4ecef8ad9ca31a8372d0c353", certificate) + + if assert.NoError(t, err) { + assert.Equal(t, certificate, actual) + } +} + +func TestRestartAdvancedCertificateValidation(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PATCH", "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b", + "type": "advanced", + "hosts": [ + "example.com", + "*.example.com", + "www.example.com" + ], + "status": "initializing", + "validation_method": "txt", + "validity_days": 365, + "certificate_authority": "digicert", + "cloudflare_branding": false + } +}`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs/3822ff90-ea29-44df-9e55-21300bb9419b", handler) + + certificate := CertificatePackAdvancedCertificate{ + Type: "advanced", + Hosts: []string{"example.com", "*.example.com", "www.example.com"}, + ValidityDays: 365, + ValidationMethod: "txt", + CertificateAuthority: "digicert", + CloudflareBranding: false, + } + + actual, err := client.RestartAdvancedCertificateValidation("023e105f4ecef8ad9ca31a8372d0c353", "3822ff90-ea29-44df-9e55-21300bb9419b") + + if assert.NoError(t, err) { + assert.Equal(t, certificate, actual) + } +} + +func TestDeleteCertificatePack(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE", "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "3822ff90-ea29-44df-9e55-21300bb9419b" + } +} + `) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/ssl/certificate_packs/3822ff90-ea29-44df-9e55-21300bb9419b", handler) + + err := client.DeleteCertificatePack("023e105f4ecef8ad9ca31a8372d0c353", "3822ff90-ea29-44df-9e55-21300bb9419b") + + assert.NoError(t, err) +}