Repository: cloudflare/cloudflare-go
Pull request #1356 (2023-08-03 22:58:10)

add per-hostname tls settings api and tests
## Description

Per-Hostname TLS Settings is a new API where customers can set TLS settings on a per-hostname basis while not being coupled to any specific certificate. SSL/TLS team.

## Has your change been tested?

Added unit tests.

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/1356.txt b/.changelog/1356.txt
new file mode 100644
index 000000000..dd94e72a3
--- /dev/null
+++ b/.changelog/1356.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+per_hostname_tls_settings: add support for managing hostname level TLS settings
+```
diff --git a/per_hostname_tls_settings.go b/per_hostname_tls_settings.go
new file mode 100644
index 000000000..26beec884
--- /dev/null
+++ b/per_hostname_tls_settings.go
@@ -0,0 +1,250 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// HostnameTLSSetting represents the metadata for a user-created tls setting.
+type HostnameTLSSetting struct {
+	Hostname  string     `json:"hostname"`
+	Value     string     `json:"value"`
+	Status    string     `json:"status"`
+	CreatedAt *time.Time `json:"created_at"`
+	UpdatedAt *time.Time `json:"updated_at"`
+}
+
+// HostnameTLSSettingResponse represents the response from the PUT and DELETE endpoints for per-hostname tls settings.
+type HostnameTLSSettingResponse struct {
+	Response
+	Result HostnameTLSSetting `json:"result"`
+}
+
+// HostnameTLSSettingsResponse represents the response from the retrieval endpoint for per-hostname tls settings.
+type HostnameTLSSettingsResponse struct {
+	Response
+	Result     []HostnameTLSSetting `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// ListHostnameTLSSettingsParams represents the data related to per-hostname tls settings being retrieved.
+type ListHostnameTLSSettingsParams struct {
+	Setting string
+	PaginationOptions
+	Limit    int      `url:"limit,omitempty"`
+	Offset   int      `url:"offset,omitempty"`
+	Hostname []string `url:"hostname,omitempty"`
+}
+
+// UpdateHostnameTLSSettingParams represents the data related to the per-hostname tls setting being updated.
+type UpdateHostnameTLSSettingParams struct {
+	Setting  string
+	Hostname string
+	Value    string `json:"value"`
+}
+
+// DeleteHostnameTLSSettingParams represents the data related to the per-hostname tls setting being deleted.
+type DeleteHostnameTLSSettingParams struct {
+	Setting  string
+	Hostname string
+}
+
+var (
+	ErrMissingHostnameTLSSettingName = errors.New("tls setting name required but missing")
+)
+
+// ListHostnameTLSSettings returns a list of all user-created tls setting values for the specified setting and hostnames.
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-list
+func (api *API) ListHostnameTLSSettings(ctx context.Context, rc *ResourceContainer, params ListHostnameTLSSettingsParams) ([]HostnameTLSSetting, ResultInfo, error) {
+	if rc.Identifier == "" {
+		return []HostnameTLSSetting{}, ResultInfo{}, ErrMissingZoneID
+	}
+	if params.Setting == "" {
+		return []HostnameTLSSetting{}, ResultInfo{}, ErrMissingHostnameTLSSettingName
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/%s", rc.Identifier, params.Setting)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, params)
+	if err != nil {
+		return []HostnameTLSSetting{}, ResultInfo{}, err
+	}
+	var r HostnameTLSSettingsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return []HostnameTLSSetting{}, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, r.ResultInfo, err
+}
+
+// UpdateHostnameTLSSetting will update the per-hostname tls setting for the specified hostname.
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-put
+func (api *API) UpdateHostnameTLSSetting(ctx context.Context, rc *ResourceContainer, params UpdateHostnameTLSSettingParams) (HostnameTLSSetting, error) {
+	if rc.Identifier == "" {
+		return HostnameTLSSetting{}, ErrMissingZoneID
+	}
+	if params.Setting == "" {
+		return HostnameTLSSetting{}, ErrMissingHostnameTLSSettingName
+	}
+	if params.Hostname == "" {
+		return HostnameTLSSetting{}, ErrMissingHostname
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/%s/%s", rc.Identifier, params.Setting, params.Hostname)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return HostnameTLSSetting{}, err
+	}
+	var r HostnameTLSSettingResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return HostnameTLSSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// DeleteHostnameTLSSetting will delete the specified per-hostname tls setting.
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-delete
+func (api *API) DeleteHostnameTLSSetting(ctx context.Context, rc *ResourceContainer, params DeleteHostnameTLSSettingParams) (HostnameTLSSetting, error) {
+	if rc.Identifier == "" {
+		return HostnameTLSSetting{}, ErrMissingZoneID
+	}
+	if params.Setting == "" {
+		return HostnameTLSSetting{}, ErrMissingHostnameTLSSettingName
+	}
+	if params.Hostname == "" {
+		return HostnameTLSSetting{}, ErrMissingHostname
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/%s/%s", rc.Identifier, params.Setting, params.Hostname)
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return HostnameTLSSetting{}, err
+	}
+	var r HostnameTLSSettingResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return HostnameTLSSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// HostnameTLSSettingCiphers represents the metadata for a user-created ciphers tls setting.
+type HostnameTLSSettingCiphers struct {
+	Hostname  string     `json:"hostname"`
+	Value     []string   `json:"value"`
+	Status    string     `json:"status"`
+	CreatedAt *time.Time `json:"created_at"`
+	UpdatedAt *time.Time `json:"updated_at"`
+}
+
+// HostnameTLSSettingCiphersResponse represents the response from the PUT and DELETE endpoints for per-hostname ciphers tls settings.
+type HostnameTLSSettingCiphersResponse struct {
+	Response
+	Result HostnameTLSSettingCiphers `json:"result"`
+}
+
+// HostnameTLSSettingsCiphersResponse represents the response from the retrieval endpoint for per-hostname ciphers tls settings.
+type HostnameTLSSettingsCiphersResponse struct {
+	Response
+	Result     []HostnameTLSSettingCiphers `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// ListHostnameTLSSettingsCiphersParams represents the data related to per-hostname ciphers tls settings being retrieved.
+type ListHostnameTLSSettingsCiphersParams struct {
+	PaginationOptions
+	Limit    int      `url:"limit,omitempty"`
+	Offset   int      `url:"offset,omitempty"`
+	Hostname []string `url:"hostname,omitempty"`
+}
+
+// UpdateHostnameTLSSettingCiphersParams represents the data related to the per-hostname ciphers tls setting being updated.
+type UpdateHostnameTLSSettingCiphersParams struct {
+	Hostname string
+	Value    []string `json:"value"`
+}
+
+// DeleteHostnameTLSSettingCiphersParams represents the data related to the per-hostname ciphers tls setting being deleted.
+type DeleteHostnameTLSSettingCiphersParams struct {
+	Hostname string
+}
+
+// ListHostnameTLSSettingsCiphers returns a list of all user-created tls setting ciphers values for the specified setting and hostnames.
+// Ciphers functions are separate due to the API returning a list of strings as the value, rather than a string (as is the case for the other tls settings).
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-list
+func (api *API) ListHostnameTLSSettingsCiphers(ctx context.Context, rc *ResourceContainer, params ListHostnameTLSSettingsCiphersParams) ([]HostnameTLSSettingCiphers, ResultInfo, error) {
+	if rc.Identifier == "" {
+		return []HostnameTLSSettingCiphers{}, ResultInfo{}, ErrMissingZoneID
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/ciphers", rc.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, params)
+	if err != nil {
+		return []HostnameTLSSettingCiphers{}, ResultInfo{}, err
+	}
+	var r HostnameTLSSettingsCiphersResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return []HostnameTLSSettingCiphers{}, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, r.ResultInfo, err
+}
+
+// UpdateHostnameTLSSettingCiphers will update the per-hostname ciphers tls setting for the specified hostname.
+// Ciphers functions are separate due to the API returning a list of strings as the value, rather than a string (as is the case for the other tls settings).
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-put
+func (api *API) UpdateHostnameTLSSettingCiphers(ctx context.Context, rc *ResourceContainer, params UpdateHostnameTLSSettingCiphersParams) (HostnameTLSSettingCiphers, error) {
+	if rc.Identifier == "" {
+		return HostnameTLSSettingCiphers{}, ErrMissingZoneID
+	}
+	if params.Hostname == "" {
+		return HostnameTLSSettingCiphers{}, ErrMissingHostname
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/ciphers/%s", rc.Identifier, params.Hostname)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return HostnameTLSSettingCiphers{}, err
+	}
+	var r HostnameTLSSettingCiphersResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return HostnameTLSSettingCiphers{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// DeleteHostnameTLSSettingCiphers will delete the specified per-hostname ciphers tls setting value.
+// Ciphers functions are separate due to the API returning a list of strings as the value, rather than a string (as is the case for the other tls settings).
+//
+// API reference: https://developers.cloudflare.com/api/operations/per-hostname-tls-settings-delete
+func (api *API) DeleteHostnameTLSSettingCiphers(ctx context.Context, rc *ResourceContainer, params DeleteHostnameTLSSettingCiphersParams) (HostnameTLSSettingCiphers, error) {
+	if rc.Identifier == "" {
+		return HostnameTLSSettingCiphers{}, ErrMissingZoneID
+	}
+	if params.Hostname == "" {
+		return HostnameTLSSettingCiphers{}, ErrMissingHostname
+	}
+
+	uri := fmt.Sprintf("/zones/%s/hostnames/settings/ciphers/%s", rc.Identifier, params.Hostname)
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return HostnameTLSSettingCiphers{}, err
+	}
+	// Unmarshal into HostnameTLSSettingResponse first because the API returns an empty string
+	var r HostnameTLSSettingResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return HostnameTLSSettingCiphers{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return HostnameTLSSettingCiphers{
+		Hostname:  r.Result.Hostname,
+		Value:     []string{},
+		Status:    r.Result.Status,
+		CreatedAt: r.Result.CreatedAt,
+		UpdatedAt: r.Result.UpdatedAt,
+	}, nil
+}


Test patch:
diff --git a/per_hostname_tls_settings_test.go b/per_hostname_tls_settings_test.go
new file mode 100644
index 000000000..b857d2eb6
--- /dev/null
+++ b/per_hostname_tls_settings_test.go
@@ -0,0 +1,273 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestListHostnameTLSSettingsMinTLSVersion(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"hostname": "app.example.com",
+					"value": "1.2",
+					"status": "active",
+					"created_at": "2023-07-26T21:12:55.56942Z",
+					"updated_at": "2023-07-31T22:06:44.739794Z"
+				}
+			],
+			"result_info": {
+				"page": 1,
+				"per_page": 50,
+				"count": 1,
+				"total_count": 1,
+				"total_pages": 1
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/min_tls_version", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+	want := []HostnameTLSSetting{
+		{
+			Hostname:  "app.example.com",
+			Value:     "1.2",
+			Status:    "active",
+			CreatedAt: &createdAt,
+			UpdatedAt: &updatedAt,
+		},
+	}
+
+	actual, _, err := client.ListHostnameTLSSettings(context.Background(), ZoneIdentifier(testZoneID), ListHostnameTLSSettingsParams{Setting: "min_tls_version"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateHostnameTLSSettingMinTLSVersion(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"hostname": "app.example.com",
+				"value": "1.2",
+				"status": "active",
+				"created_at": "2023-07-26T21:12:55.56942Z",
+				"updated_at": "2023-07-31T22:06:44.739794Z"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/min_tls_version/app.example.com", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+
+	want := HostnameTLSSetting{
+		Hostname:  "app.example.com",
+		Value:     "1.2",
+		Status:    "active",
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+	}
+
+	params := UpdateHostnameTLSSettingParams{
+		Setting:  "min_tls_version",
+		Hostname: "app.example.com",
+		Value:    "1.2",
+	}
+	actual, err := client.UpdateHostnameTLSSetting(context.Background(), ZoneIdentifier(testZoneID), params)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestDeleteHostnameTLSSettingMinTLSVersion(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"hostname": "app.example.com",
+				"value": "",
+				"status": "active",
+				"created_at": "2023-07-26T21:12:55.56942Z",
+				"updated_at": "2023-07-31T22:06:44.739794Z"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/min_tls_version/app.example.com", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+	want := HostnameTLSSetting{
+		Hostname:  "app.example.com",
+		Value:     "",
+		Status:    "active",
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+	}
+
+	actual, err := client.DeleteHostnameTLSSetting(context.Background(), ZoneIdentifier(testZoneID), DeleteHostnameTLSSettingParams{Setting: "min_tls_version", Hostname: "app.example.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestListHostnameTLSSettingsCiphers(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"hostname": "app.example.com",
+					"value": [
+						"AES128-GCM-SHA256",
+						"ECDHE-RSA-AES128-GCM-SHA256"
+					],
+					"status": "active",
+					"created_at": "2023-07-26T21:12:55.56942Z",
+					"updated_at": "2023-07-31T22:06:44.739794Z"
+				}
+			],
+			"result_info": {
+				"page": 1,
+				"per_page": 50,
+				"count": 1,
+				"total_count": 1,
+				"total_pages": 1
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/ciphers", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+
+	want := []HostnameTLSSettingCiphers{
+		{
+			Hostname:  "app.example.com",
+			Value:     []string{"AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256"},
+			Status:    "active",
+			CreatedAt: &createdAt,
+			UpdatedAt: &updatedAt,
+		},
+	}
+
+	actual, _, err := client.ListHostnameTLSSettingsCiphers(context.Background(), ZoneIdentifier(testZoneID), ListHostnameTLSSettingsCiphersParams{})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateHostnameTLSSettingCiphers(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"hostname": "app.example.com",
+				"value": [
+					"AES128-GCM-SHA256",
+					"ECDHE-RSA-AES128-GCM-SHA256"
+				],
+				"status": "active",
+				"created_at": "2023-07-26T21:12:55.56942Z",
+				"updated_at": "2023-07-31T22:06:44.739794Z"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/ciphers/app.example.com", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+
+	want := HostnameTLSSettingCiphers{
+		Hostname:  "app.example.com",
+		Value:     []string{"AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256"},
+		Status:    "active",
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+	}
+
+	params := UpdateHostnameTLSSettingCiphersParams{
+		Hostname: "app.example.com",
+		Value:    []string{"AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256"},
+	}
+	actual, err := client.UpdateHostnameTLSSettingCiphers(context.Background(), ZoneIdentifier(testZoneID), params)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestDeleteHostnameTLSSettingCiphers(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"hostname": "app.example.com",
+				"value": "",
+				"status": "active",
+				"created_at": "2023-07-26T21:12:55.56942Z",
+				"updated_at": "2023-07-31T22:06:44.739794Z"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/hostnames/settings/ciphers/app.example.com", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2023-07-26T21:12:55.56942Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2023-07-31T22:06:44.739794Z")
+	want := HostnameTLSSettingCiphers{
+		Hostname:  "app.example.com",
+		Value:     []string{},
+		Status:    "active",
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+	}
+
+	actual, err := client.DeleteHostnameTLSSettingCiphers(context.Background(), ZoneIdentifier(testZoneID), DeleteHostnameTLSSettingCiphersParams{Hostname: "app.example.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}