gcloud/clouddomains.go (204 lines of code) (raw):

// Copyright 2023 Google LLC // // 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 gcloud import ( "bytes" "context" "fmt" "io" "text/template" domains "cloud.google.com/go/domains/apiv1beta1" "google.golang.org/api/iterator" domainspb "google.golang.org/genproto/googleapis/cloud/domains/v1beta1" "google.golang.org/genproto/googleapis/type/postaladdress" "gopkg.in/yaml.v2" ) var ( // ErrorDomainUntenable is returned when a domain isn't available for registration, but // is also not owned by the user. It can't be used in this app ErrorDomainUntenable = fmt.Errorf("domain is not available, and not owned by attempting user") // ErrorDomainUserDeny is returned when an user declines the choice to purchase. ErrorDomainUserDeny = fmt.Errorf("user said no to buying the domain") ) func (c *Client) getDomainsClient(project string) (*domains.Client, error) { var err error svc := c.services.domains if svc != nil { return svc, nil } if err := c.ServiceEnable(project, Domains); err != nil { return nil, fmt.Errorf("error activating service for polling: %s", err) } svc, err = domains.NewClient(c.ctx, c.opts) if err != nil { return nil, fmt.Errorf("could not retrieve service: %w", err) } c.services.domains = svc return svc, nil } // ContactData represents the structure that we need for Registrar Contact // Data type ContactData struct { AllContacts DomainRegistrarContact `yaml:"allContacts"` } // WriteTo writes the content of ContactData to a writer func (c ContactData) WriteTo(w io.Writer) (int64, error) { yaml, err := c.YAML() if err != nil { return 0, fmt.Errorf("contactdata.writeto: cannot convert to yaml: %w", err) } result, err := w.Write([]byte(yaml)) if err != nil { return 0, fmt.Errorf("contactdata.writeto: cannot write %w", err) } return int64(result), nil } // ReadFrom populates the content of ContactData from a reader func (c *ContactData) ReadFrom(r io.Reader) (int64, error) { dat, err := io.ReadAll(r) err = yaml.Unmarshal(dat, c) return 0, err } // DomainRegistrarContact represents the data required to register a domain // with a public registrar. type DomainRegistrarContact struct { Email string `yaml:"email"` Phone string `yaml:"phoneNumber"` PostalAddress PostalAddress `yaml:"postalAddress"` } // PostalAddress represents the mail address in a DomainRegistrarContact type PostalAddress struct { RegionCode string `yaml:"regionCode"` PostalCode string `yaml:"postalCode"` AdministrativeArea string `yaml:"administrativeArea"` Locality string `yaml:"locality"` AddressLines []string `yaml:"addressLines"` Recipients []string `yaml:"recipients"` } // YAML outputs the content of this structure into the contact format needed for // domain registration func (c ContactData) YAML() (string, error) { yaml := `allContacts: email: '{{ .AllContacts.Email}}' phoneNumber: '{{.AllContacts.Phone}}' postalAddress: regionCode: '{{ .AllContacts.PostalAddress.RegionCode}}' postalCode: '{{ .AllContacts.PostalAddress.PostalCode}}' administrativeArea: '{{ .AllContacts.PostalAddress.AdministrativeArea}}' locality: '{{ .AllContacts.PostalAddress.Locality}}' addressLines: [{{range $element := .AllContacts.PostalAddress.AddressLines}}'{{$element}}'{{end}}] recipients: [{{range $element := .AllContacts.PostalAddress.Recipients}}'{{$element}}'{{end}}]` t, err := template.New("yaml").Parse(yaml) if err != nil { return "", fmt.Errorf("error parsing the yaml template %s", err) } var tpl bytes.Buffer err = t.Execute(&tpl, c) if err != nil { return "", fmt.Errorf("error executing the yaml template %s", err) } return tpl.String(), nil } // DomainContact outputs a varible in the format that Domain Registration // API needs. func (c ContactData) DomainContact() (domainspb.ContactSettings, error) { dc := domainspb.ContactSettings{} pa := postaladdress.PostalAddress{ RegionCode: c.AllContacts.PostalAddress.RegionCode, PostalCode: c.AllContacts.PostalAddress.PostalCode, AdministrativeArea: c.AllContacts.PostalAddress.AdministrativeArea, Locality: c.AllContacts.PostalAddress.Locality, AddressLines: c.AllContacts.PostalAddress.AddressLines, Recipients: c.AllContacts.PostalAddress.Recipients, } all := domainspb.ContactSettings_Contact{ Email: c.AllContacts.Email, PhoneNumber: c.AllContacts.Phone, PostalAddress: &pa, } dc.AdminContact = &all dc.RegistrantContact = &all dc.TechnicalContact = &all dc.Privacy = domainspb.ContactPrivacy_PRIVATE_CONTACT_DATA return dc, nil } func newContactData() ContactData { c := ContactData{} d := DomainRegistrarContact{} d.PostalAddress.AddressLines = []string{} d.PostalAddress.Recipients = []string{} c.AllContacts = d return c } // DomainsSearch checks the Cloud Domain api for the input domain func (c Client) DomainsSearch(project, domain string) ([]*domainspb.RegisterParameters, error) { svc, err := c.getDomainsClient(project) if err != nil { return nil, err } req := &domainspb.SearchDomainsRequest{ Query: domain, Location: fmt.Sprintf("projects/%s/locations/global", project), } resp, err := svc.SearchDomains(context.Background(), req) if err != nil { return nil, err } return resp.RegisterParameters, nil } // DomainIsAvailable checks to see if a given domain is available for // registration func (c Client) DomainIsAvailable(project, domain string) (*domainspb.RegisterParameters, error) { list, err := c.DomainsSearch(project, domain) if err != nil { return nil, err } for _, v := range list { if v.DomainName == domain { return v, err } } return nil, err } // DomainIsVerified checks to see if a given domain belongs to this user func (c Client) DomainIsVerified(project, domain string) (bool, error) { svc, err := c.getDomainsClient(project) if err != nil { return false, fmt.Errorf("cannot get domains client: %s", err) } req := &domainspb.ListRegistrationsRequest{ Filter: fmt.Sprintf("domainName=\"%s\"", domain), Parent: fmt.Sprintf("projects/%s/locations/global", project), } it := svc.ListRegistrations(c.ctx, req) for { resp, err := it.Next() if err == iterator.Done { break } if err != nil { return false, fmt.Errorf("listing domains failed: %s", err) } if resp.DomainName == domain { return true, nil } } return false, nil } // DomainRegister handles registring a domain on behalf of the user. func (c Client) DomainRegister(project string, domaininfo *domainspb.RegisterParameters, contact ContactData) error { parent := fmt.Sprintf("projects/%s/locations/global", project) svc, err := c.getDomainsClient(project) if err != nil { return err } dnscontact, err := contact.DomainContact() if err != nil { return err } req := &domainspb.RegisterDomainRequest{ DomainNotices: domaininfo.DomainNotices, Registration: &domainspb.Registration{ Name: fmt.Sprintf("%s/registrations/%s", parent, domaininfo.DomainName), DomainName: domaininfo.DomainName, DnsSettings: &domainspb.DnsSettings{ DnsProvider: &domainspb.DnsSettings_CustomDns_{ CustomDns: &domainspb.DnsSettings_CustomDns{ NameServers: []string{ "ns-cloud-e1.googledomains.com", "ns-cloud-e2.googledomains.com", "ns-cloud-e3.googledomains.com", "ns-cloud-e4.googledomains.com", }, }, }, }, ContactSettings: &dnscontact, }, Parent: parent, YearlyPrice: domaininfo.YearlyPrice, } if _, err := svc.RegisterDomain(c.ctx, req); err != nil { return err } return nil }