From 86147daf9238e0cb675f8683f07b0ff64c3814c7 Mon Sep 17 00:00:00 2001 From: Elvin Efendi Date: Sat, 13 May 2017 08:53:00 -0400 Subject: [PATCH] implement interface to manage custom hostnames (#123) * implement interfcae to manage custom hostnames * place holder UpdateCustomHostnameSSL function * improve documentation --- README.md | 1 + custom_hostname.go | 149 +++++++++++++++++++++++++++++++++ custom_hostname_test.go | 177 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 custom_hostname.go create mode 100644 custom_hostname_test.go diff --git a/README.md b/README.md index 523e9b3e8f4..d2e622ab9b0 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The current feature list includes: - [x] Cloudflare IPs - [x] User Administration (partial) - [x] Virtual DNS Management +- [x] Custom hostnames - [ ] Organization Administration - [ ] [Railgun](https://www.cloudflare.com/railgun/) administration - [ ] [Keyless SSL](https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/) diff --git a/custom_hostname.go b/custom_hostname.go new file mode 100644 index 00000000000..44dbc4068f0 --- /dev/null +++ b/custom_hostname.go @@ -0,0 +1,149 @@ +package cloudflare + +import ( + "encoding/json" + "net/url" + "strconv" + + "github.com/pkg/errors" +) + +// CustomHostnameSSL represents the SSL section in a given custom hostname. +type CustomHostnameSSL struct { + Status string `json:"status,omitempty"` + Method string `json:"method,omitempty"` + Type string `json:"type,omitempty"` + CnameTarget string `json:"cname_target,omitempty"` + CnameName string `json:"cname_name,omitempty"` +} + +// CustomMetadata defines custom metadata for the hostname. This requires logic to be implemented by Cloudflare to act on the data provided. +type CustomMetadata map[string]interface{} + +// CustomHostname represents a custom hostname in a zone. +type CustomHostname struct { + ID string `json:"id,omitempty"` + Hostname string `json:"hostname,omitempty"` + SSL CustomHostnameSSL `json:"ssl,omitempty"` + CustomMetadata CustomMetadata `json:"custom_metadata,omitempty"` +} + +// CustomHostNameResponse represents a response from the Custom Hostnames endpoints. +type CustomHostnameResponse struct { + Result CustomHostname `json:"result"` + Response +} + +// CustomHostnameListResponse represents a response from the Custom Hostnames endpoints. +type CustomHostnameListResponse struct { + Result []CustomHostname `json:"result"` + Response + ResultInfo `json:"result_info"` +} + +// Modify SSL configuration for the given custom hostname in the given zone. +// +// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-update-custom-hostname-configuration +func (api *API) UpdateCustomHostnameSSL(zoneID string, customHostnameID string, ssl CustomHostnameSSL) (CustomHostname, error) { + return CustomHostname{}, errors.New("Not implemented") +} + +// Delete a custom hostname (and any issued SSL certificates) +// +// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-delete-a-custom-hostname-and-any-issued-ssl-certificates- +func (api *API) DeleteCustomHostname(zoneID string, customHostnameID string) error { + uri := "/zones/" + zoneID + "/custom_hostnames/" + customHostnameID + res, err := api.makeRequest("DELETE", uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + + var response *CustomHostnameResponse + err = json.Unmarshal(res, &response) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + + return nil +} + +// CreateCustomHostname creates a new custom hostname and requests that an SSL certificate be issued for it. +// +// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-create-custom-hostname +func (api *API) CreateCustomHostname(zoneID string, ch CustomHostname) (*CustomHostnameResponse, error) { + uri := "/zones/" + zoneID + "/custom_hostnames" + res, err := api.makeRequest("POST", uri, ch) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + + var response *CustomHostnameResponse + err = json.Unmarshal(res, &response) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + + return response, nil +} + +// CustomHostnames fetches custom hostnames for the given zone, +// by applying filter.Hostname if not empty and scoping the result to page'th 50 items. +// +// The returned ResultInfo can be used to implement pagination. +// +// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-list-custom-hostnames +func (api *API) CustomHostnames(zoneID string, page int, filter CustomHostname) ([]CustomHostname, ResultInfo, error) { + v := url.Values{} + v.Set("per_page", "50") + v.Set("page", strconv.Itoa(page)) + if filter.Hostname != "" { + v.Set("hostname", filter.Hostname) + } + query := "?" + v.Encode() + + uri := "/zones/" + zoneID + "/custom_hostnames" + query + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return []CustomHostname{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError) + } + var customHostnameListResponse CustomHostnameListResponse + err = json.Unmarshal(res, &customHostnameListResponse) + if err != nil { + return []CustomHostname{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError) + } + + return customHostnameListResponse.Result, customHostnameListResponse.ResultInfo, nil +} + +// CustomHostname inspects the given custom hostname in the given zone. +// +// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-custom-hostname-configuration-details +func (api *API) CustomHostname(zoneID string, customHostnameID string) (CustomHostname, error) { + uri := "/zones/" + zoneID + "/custom_hostnames/" + customHostnameID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return CustomHostname{}, errors.Wrap(err, errMakeRequestError) + } + + var response CustomHostnameResponse + err = json.Unmarshal(res, &response) + if err != nil { + return CustomHostname{}, errors.Wrap(err, errUnmarshalError) + } + + return response.Result, nil +} + +// CustomHostnameIDByName retrieves the ID for the given hostname in the given zone. +func (api *API) CustomHostnameIDByName(zoneID string, hostname string) (string, error) { + customHostnames, _, err := api.CustomHostnames(zoneID, 1, CustomHostname{Hostname: hostname}) + if err != nil { + return "", errors.Wrap(err, "CustomHostnames command failed") + } + for _, ch := range customHostnames { + if ch.Hostname == hostname { + return ch.ID, nil + } + } + return "", errors.New("CustomHostname could not be found") +} diff --git a/custom_hostname_test.go b/custom_hostname_test.go new file mode 100644 index 00000000000..9afc3debebf --- /dev/null +++ b/custom_hostname_test.go @@ -0,0 +1,177 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCustomHostname_DeleteCustomHostname(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/foo/custom_hostnames/bar", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method, "Expected method 'DELETE', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, ` +{ + "id": "bar" +}`) + }) + + err := client.DeleteCustomHostname("foo", "bar") + + assert.NoError(t, err) +} + +func TestCustomHostname_CreateCustomHostname(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/foo/custom_hostnames", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method, "Expected method 'POST', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "0d89c70d-ad9f-4843-b99f-6cc0252067e9", + "hostname": "app.example.com", + "ssl": { + "status": "pending_validation", + "method": "cname", + "type": "dv", + "cname_target": "dcv.digicert.com", + "cname_name": "810b7d5f01154524b961ba0cd578acc2.app.example.com" + } + } +}`) + }) + + response, err := client.CreateCustomHostname("foo", CustomHostname{Hostname: "app.example.com", SSL: CustomHostnameSSL{Method: "cname", Type: "dv"}}) + + want := &CustomHostnameResponse{ + Result: CustomHostname{ + ID: "0d89c70d-ad9f-4843-b99f-6cc0252067e9", + Hostname: "app.example.com", + SSL: CustomHostnameSSL{ + Type: "dv", + Method: "cname", + Status: "pending_validation", + CnameTarget: "dcv.digicert.com", + CnameName: "810b7d5f01154524b961ba0cd578acc2.app.example.com", + }, + }, + Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}}, + } + + if assert.NoError(t, err) { + assert.Equal(t, want, response) + } +} + +func TestCustomHostname_CustomHostnames(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/foo/custom_hostnames", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ +"success": true, +"result": [ + { + "id": "custom_host_1", + "hostname": "custom.host.one", + "ssl": { + "type": "dv", + "method": "cname", + "status": "pending_validation", + "cname_target": "dcv.digicert.com", + "cname_name": "810b7d5f01154524b961ba0cd578acc2.app.example.com" + }, + "custom_metadata": { + "a_random_field": "random field value" + } + } +], +"result_info": { + "page": 1, + "per_page": 20, + "count": 5, + "total_count": 5 +} +}`) + }) + + customHostnames, _, err := client.CustomHostnames("foo", 1, CustomHostname{}) + + want := []CustomHostname{ + { + ID: "custom_host_1", + Hostname: "custom.host.one", + SSL: CustomHostnameSSL{ + Type: "dv", + Method: "cname", + Status: "pending_validation", + CnameTarget: "dcv.digicert.com", + CnameName: "810b7d5f01154524b961ba0cd578acc2.app.example.com", + }, + CustomMetadata: CustomMetadata{"a_random_field": "random field value"}, + }, + } + + if assert.NoError(t, err) { + assert.Equal(t, want, customHostnames) + } +} + +func TestCustomHostname_CustomHostname(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/foo/custom_hostnames/bar", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ +"success": true, +"result": { + "id": "bar", + "hostname": "foo.bar.com", + "ssl": { + "type": "dv", + "method": "http", + "status": "active" + }, + "custom_metadata": { + "origin": "a.custom.origin" + } + } +}`) + }) + + customHostname, err := client.CustomHostname("foo", "bar") + + want := CustomHostname{ + ID: "bar", + Hostname: "foo.bar.com", + SSL: CustomHostnameSSL{ + Status: "active", + Method: "http", + Type: "dv", + }, + CustomMetadata: CustomMetadata{"origin": "a.custom.origin"}, + } + + if assert.NoError(t, err) { + assert.Equal(t, want, customHostname) + } +}