diff --git a/Gopkg.lock b/Gopkg.lock index 22b19271..ad376567 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -446,6 +446,18 @@ pruneopts = "NUT" revision = "37e84520dcf74488f67654f9c775b9752c232dc1" +[[projects]] + branch = "master" + digest = "1:3b236e8930d31aeb375fe405c15c2afc581e04bd6cb68da4723e1aa8d2e2da37" + name = "github.com/transip/gotransip" + packages = [ + ".", + "domain", + "util", + ] + pruneopts = "NUT" + revision = "1dc93a7db3567a5ccf865106afac88278ba940cf" + [[projects]] digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58" name = "github.com/urfave/cli" @@ -659,6 +671,8 @@ "github.com/stretchr/testify/suite", "github.com/timewasted/linode", "github.com/timewasted/linode/dns", + "github.com/transip/gotransip", + "github.com/transip/gotransip/domain", "github.com/urfave/cli", "golang.org/x/crypto/ocsp", "golang.org/x/net/context", diff --git a/Gopkg.toml b/Gopkg.toml index af2670a8..9c365a4c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,6 +85,10 @@ branch = "master" name = "github.com/sacloud/libsacloud" +[[constraint]] + branch = "master" + name = "github.com/transip/gotransip" + [[constraint]] version = "0.11.1" name = "github.com/exoscale/egoscale" diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 1d7672a4..4a50a8a3 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -48,6 +48,7 @@ import ( "github.com/xenolf/lego/providers/dns/sakuracloud" "github.com/xenolf/lego/providers/dns/selectel" "github.com/xenolf/lego/providers/dns/stackpath" + "github.com/xenolf/lego/providers/dns/transip" "github.com/xenolf/lego/providers/dns/vegadns" "github.com/xenolf/lego/providers/dns/vultr" ) @@ -145,6 +146,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return stackpath.NewDNSProvider() case "selectel": return selectel.NewDNSProvider() + case "transip": + return transip.NewDNSProvider() case "vegadns": return vegadns.NewDNSProvider() case "vultr": diff --git a/providers/dns/transip/fixtures/private.key b/providers/dns/transip/fixtures/private.key new file mode 100644 index 00000000..e69de29b diff --git a/providers/dns/transip/transip.go b/providers/dns/transip/transip.go new file mode 100644 index 00000000..aeb155f8 --- /dev/null +++ b/providers/dns/transip/transip.go @@ -0,0 +1,150 @@ +// Package transip implements a DNS provider for solving the DNS-01 challenge using TransIP. +package transip + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/transip/gotransip" + transipdomain "github.com/transip/gotransip/domain" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + AccountName string + PrivateKeyPath string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int64 +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + TTL: int64(env.GetOrDefaultInt("TRANSIP_TTL", 10)), + PropagationTimeout: env.GetOrDefaultSecond("TRANSIP_PROPAGATION_TIMEOUT", 10*time.Minute), + PollingInterval: env.GetOrDefaultSecond("TRANSIP_POLLING_INTERVAL", 10*time.Second), + } +} + +// DNSProvider describes a provider for TransIP +type DNSProvider struct { + config *Config + client gotransip.SOAPClient +} + +// NewDNSProvider returns a DNSProvider instance configured for TransIP. +// Credentials must be passed in the environment variables: +// TRANSIP_ACCOUNTNAME, TRANSIP_PRIVATEKEYPATH. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("TRANSIP_ACCOUNT_NAME", "TRANSIP_PRIVATE_KEY_PATH") + if err != nil { + return nil, fmt.Errorf("transip: %v", err) + } + + config := NewDefaultConfig() + config.AccountName = values["TRANSIP_ACCOUNT_NAME"] + config.PrivateKeyPath = values["TRANSIP_PRIVATE_KEY_PATH"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for TransIP. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("transip: the configuration of the DNS provider is nil") + } + + client, err := gotransip.NewSOAPClient(gotransip.ClientConfig{ + AccountName: config.AccountName, + PrivateKeyPath: config.PrivateKeyPath, + }) + if err != nil { + return nil, fmt.Errorf("transip: %v", err) + } + + return &DNSProvider{client: client, config: config}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + domainName := acme.UnFqdn(authZone) + + // get the subDomain + subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + + // get all DNS entries + info, err := transipdomain.GetInfo(d.client, domainName) + if err != nil { + return fmt.Errorf("transip: error for %s in Present: %v", domain, err) + } + + // include the new DNS entry + dnsEntries := append(info.DNSEntries, transipdomain.DNSEntry{ + Name: subDomain, + TTL: d.config.TTL, + Type: transipdomain.DNSEntryTypeTXT, + Content: value, + }) + + // set the updated DNS entries + err = transipdomain.SetDNSEntries(d.client, domainName, dnsEntries) + if err != nil { + return fmt.Errorf("transip: %v", err) + } + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + domainName := acme.UnFqdn(authZone) + + // get the subDomain + subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + + // get all DNS entries + info, err := transipdomain.GetInfo(d.client, domainName) + if err != nil { + return fmt.Errorf("transip: error for %s in CleanUp: %v", fqdn, err) + } + + // loop through the existing entries and remove the specific record + updatedEntries := info.DNSEntries[:0] + for _, e := range info.DNSEntries { + if e.Name != subDomain { + updatedEntries = append(updatedEntries, e) + } + } + + // set the updated DNS entries + err = transipdomain.SetDNSEntries(d.client, domainName, updatedEntries) + if err != nil { + return fmt.Errorf("transip: couldn't get Record ID in CleanUp: %sv", err) + } + + return nil +} diff --git a/providers/dns/transip/transip_test.go b/providers/dns/transip/transip_test.go new file mode 100644 index 00000000..35c622ca --- /dev/null +++ b/providers/dns/transip/transip_test.go @@ -0,0 +1,164 @@ +package transip + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/platform/tester" +) + +var envTest = tester.NewEnvTest( + "TRANSIP_ACCOUNT_NAME", + "TRANSIP_PRIVATE_KEY_PATH"). + WithDomain("TRANSIP_DOMAIN") + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + "TRANSIP_ACCOUNT_NAME": "johndoe", + "TRANSIP_PRIVATE_KEY_PATH": "./fixtures/private.key", + }, + }, + { + desc: "missing all credentials", + envVars: map[string]string{ + "TRANSIP_ACCOUNT_NAME": "", + "TRANSIP_PRIVATE_KEY_PATH": "", + }, + expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME,TRANSIP_PRIVATE_KEY_PATH", + }, + { + desc: "missing account name", + envVars: map[string]string{ + "TRANSIP_ACCOUNT_NAME": "", + "TRANSIP_PRIVATE_KEY_PATH": "./fixtures/private.key", + }, + expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME", + }, + { + desc: "missing private key path", + envVars: map[string]string{ + "TRANSIP_ACCOUNT_NAME": "johndoe", + "TRANSIP_PRIVATE_KEY_PATH": "", + }, + expected: "transip: some credentials information are missing: TRANSIP_PRIVATE_KEY_PATH", + }, + { + desc: "could not open private key path", + envVars: map[string]string{ + "TRANSIP_ACCOUNT_NAME": "johndoe", + "TRANSIP_PRIVATE_KEY_PATH": "./fixtures/non/existent/private.key", + }, + expected: "transip: could not open private key: stat ./fixtures/non/existent/private.key: no such file or directory", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + accountName string + privateKeyPath string + expected string + }{ + { + desc: "success", + accountName: "johndoe", + privateKeyPath: "./fixtures/private.key", + }, + { + desc: "missing all credentials", + expected: "transip: AccountName is required", + }, + { + desc: "missing account name", + privateKeyPath: "./fixtures/private.key", + expected: "transip: AccountName is required", + }, + { + desc: "missing private key path", + accountName: "johndoe", + expected: "transip: PrivateKeyPath or PrivateKeyBody is required", + }, + { + desc: "could not open private key path", + accountName: "johndoe", + privateKeyPath: "./fixtures/non/existent/private.key", + expected: "transip: could not open private key: stat ./fixtures/non/existent/private.key: no such file or directory", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AccountName = test.accountName + config.PrivateKeyPath = test.privateKeyPath + + p, err := NewDNSProviderConfig(config) + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/vendor/github.com/transip/gotransip/LICENSE b/vendor/github.com/transip/gotransip/LICENSE new file mode 100644 index 00000000..352d193e --- /dev/null +++ b/vendor/github.com/transip/gotransip/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TransIP B.V. + +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/transip/gotransip/api.go b/vendor/github.com/transip/gotransip/api.go new file mode 100644 index 00000000..46c68448 --- /dev/null +++ b/vendor/github.com/transip/gotransip/api.go @@ -0,0 +1,12 @@ +package gotransip + +// CancellationTime represents the possible ways of canceling a contract +type CancellationTime string + +var ( + // CancellationTimeEnd specifies to cancel the contract when the contract was + // due to end anyway + CancellationTimeEnd CancellationTime = "end" + // CancellationTimeImmediately specifies to cancel the contract immediately + CancellationTimeImmediately CancellationTime = "immediately" +) diff --git a/vendor/github.com/transip/gotransip/client.go b/vendor/github.com/transip/gotransip/client.go new file mode 100644 index 00000000..0be9c400 --- /dev/null +++ b/vendor/github.com/transip/gotransip/client.go @@ -0,0 +1,119 @@ +package gotransip + +import ( + "errors" + "fmt" + "io/ioutil" + "os" +) + +const ( + transipAPIHost = "api.transip.nl" + transipAPINamespace = "http://www.transip.nl/soap" +) + +// APIMode specifies in which mode the API is used. Currently this is only +// supports either readonly or readwrite +type APIMode string + +var ( + // APIModeReadOnly specifies that no changes can be made from API calls + APIModeReadOnly APIMode = "readonly" + // APIModeReadWrite specifies that changes can be made from API calls + APIModeReadWrite APIMode = "readwrite" +) + +// ClientConfig is a tool to easily create a new Client object +type ClientConfig struct { + AccountName string + PrivateKeyPath string + PrivateKeyBody []byte + Mode APIMode +} + +// Client is the interface which all clients should implement +type Client interface { + Call(SoapRequest, interface{}) error // execute request on client +} + +// SOAPClient represents a TransIP API SOAP client, implementing the Client +// interface +type SOAPClient struct { + soapClient soapClient +} + +// Call performs given SOAP request and fills the response into result +func (c SOAPClient) Call(req SoapRequest, result interface{}) error { + return c.soapClient.call(req, result) +} + +// NewSOAPClient returns a new SOAPClient object for given config +// ClientConfig's PrivateKeyPath will override potentially given PrivateKeyBody +func NewSOAPClient(c ClientConfig) (SOAPClient, error) { + // check account name + if len(c.AccountName) == 0 { + return SOAPClient{}, errors.New("AccountName is required") + } + + // check if private key was given in any form + if len(c.PrivateKeyPath) == 0 && len(c.PrivateKeyBody) == 0 { + return SOAPClient{}, errors.New("PrivateKeyPath or PrivateKeyBody is required") + } + + // if PrivateKeyPath was set, this will override any given PrivateKeyBody + if len(c.PrivateKeyPath) > 0 { + // try to open private key and read contents + if _, err := os.Stat(c.PrivateKeyPath); err != nil { + return SOAPClient{}, fmt.Errorf("could not open private key: %s", err.Error()) + } + + // read private key so we can pass the body to the soapClient + var err error + c.PrivateKeyBody, err = ioutil.ReadFile(c.PrivateKeyPath) + if err != nil { + return SOAPClient{}, err + } + } + + // default to APIMode read/write + if len(c.Mode) == 0 { + c.Mode = APIModeReadWrite + } + + // create soapClient and pass it to a new Client pointer + sc := soapClient{ + Login: c.AccountName, + Mode: c.Mode, + PrivateKey: c.PrivateKeyBody, + } + + return SOAPClient{ + soapClient: sc, + }, nil +} + +// FakeSOAPClient is a client doing nothing except implementing the gotransip.Client +// interface +// you can however set a fixture XML body which Call will try to Unmarshal into +// result +// useful for testing +type FakeSOAPClient struct { + fixture []byte // preset this fixture so Call can use it to Unmarshal +} + +// FixtureFromFile reads file and sets content as FakeSOAPClient's fixture +func (f *FakeSOAPClient) FixtureFromFile(file string) (err error) { + // read fixture file + f.fixture, err = ioutil.ReadFile(file) + if err != nil { + err = fmt.Errorf("could not read fixture from file %s: %s", file, err.Error()) + } + + return +} + +// Call doesn't do anything except fill the XML unmarshalled result +func (f FakeSOAPClient) Call(req SoapRequest, result interface{}) error { + // this fake client just parses given fixture as if it was a SOAP response + return parseSoapResponse(f.fixture, req.Padding, 200, result) +} diff --git a/vendor/github.com/transip/gotransip/domain/api.go b/vendor/github.com/transip/gotransip/domain/api.go new file mode 100644 index 00000000..f5f90f8e --- /dev/null +++ b/vendor/github.com/transip/gotransip/domain/api.go @@ -0,0 +1,314 @@ +package domain + +import ( + "github.com/transip/gotransip" +) + +// This file holds all DomainService methods directly ported from TransIP API + +// BatchCheckAvailability checks the availability of multiple domains +func BatchCheckAvailability(c gotransip.Client, domainNames []string) ([]CheckResult, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "batchCheckAvailability", + } + sr.AddArgument("domainNames", domainNames) + + var v struct { + V []CheckResult `xml:"item"` + } + + err := c.Call(sr, &v) + return v.V, err +} + +// CheckAvailability returns the availability status of a domain. +func CheckAvailability(c gotransip.Client, domainName string) (Status, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "checkAvailability", + } + sr.AddArgument("domainName", domainName) + + var v Status + err := c.Call(sr, &v) + return v, err +} + +// GetWhois returns the whois of a domain name +func GetWhois(c gotransip.Client, domainName string) (string, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getWhois", + } + sr.AddArgument("domainName", domainName) + + var v string + err := c.Call(sr, &v) + return v, err +} + +// GetDomainNames returns list with domain names or error when this failed +func GetDomainNames(c gotransip.Client) ([]string, error) { + var d = struct { + D []string `xml:"item"` + }{} + err := c.Call(gotransip.SoapRequest{ + Service: serviceName, + Method: "getDomainNames", + }, &d) + + return d.D, err +} + +// GetInfo returns Domain for given name or error when this failed +func GetInfo(c gotransip.Client, domainName string) (Domain, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getInfo", + } + sr.AddArgument("domainName", domainName) + + var d Domain + err := c.Call(sr, &d) + + return d, err +} + +// BatchGetInfo returns array of Domain for given name or error when this failed +func BatchGetInfo(c gotransip.Client, domainNames []string) ([]Domain, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "batchGetInfo", + } + sr.AddArgument("domainNames", domainNames) + + var d = struct { + D []Domain `xml:"item"` + }{} + err := c.Call(sr, &d) + + return d.D, err +} + +// GetAuthCode returns the Auth code for a domainName +func GetAuthCode(c gotransip.Client, domainName string) (string, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getAuthCode", + } + sr.AddArgument("domainName", domainName) + + var v string + err := c.Call(sr, &v) + return v, err +} + +// GetIsLocked returns the lock status for a domainName +func GetIsLocked(c gotransip.Client, domainName string) (bool, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getIsLocked", + } + sr.AddArgument("domainName", domainName) + + var v bool + err := c.Call(sr, &v) + return v, err +} + +// Register registers a domain name and will automatically create and sign a proposition for it +func Register(c gotransip.Client, domain string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "register", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} + +// Cancel cancels a domain name, will automatically create and sign a cancellation document +func Cancel(c gotransip.Client, domainName string, endTime gotransip.CancellationTime) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "cancel", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("endTime", string(endTime)) + + return c.Call(sr, nil) +} + +// TransferWithOwnerChange transfers a domain with changing the owner +func TransferWithOwnerChange(c gotransip.Client, domain, authCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "transferWithOwnerChange", + } + sr.AddArgument("domain", domain) + sr.AddArgument("authCode", authCode) + + return c.Call(sr, nil) +} + +// TransferWithoutOwnerChange transfers a domain without changing the owner +func TransferWithoutOwnerChange(c gotransip.Client, domain, authCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "transferWithoutOwnerChange", + } + sr.AddArgument("domain", domain) + sr.AddArgument("authCode", authCode) + + return c.Call(sr, nil) +} + +// SetNameservers starts a nameserver change for this domain, will replace all +// existing nameservers with the new nameservers +func SetNameservers(c gotransip.Client, domainName string, nameservers Nameservers) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setNameservers", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("nameservers", nameservers) + + return c.Call(sr, nil) +} + +// SetLock locks this domain +func SetLock(c gotransip.Client, domainName string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setLock", + } + sr.AddArgument("domainName", domainName) + + return c.Call(sr, nil) +} + +// UnsetLock unlocks this domain +func UnsetLock(c gotransip.Client, domainName string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "unsetLock", + } + sr.AddArgument("domainName", domainName) + + return c.Call(sr, nil) +} + +// SetDNSEntries sets the DnsEntries for this Domain, will replace all existing +// dns entries with the new entries +func SetDNSEntries(c gotransip.Client, domainName string, dnsEntries DNSEntries) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setDnsEntries", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("dnsEntries", dnsEntries) + + return c.Call(sr, nil) +} + +// SetOwner starts an owner change of a domain +func SetOwner(c gotransip.Client, domainName, registrantWhoisContact WhoisContact) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setOwner", + } + sr.AddArgument("domainName", domainName) + // make sure contact is of type registrant + registrantWhoisContact.Type = "registrant" + sr.AddArgument("registrantWhoisContact", registrantWhoisContact) + + return c.Call(sr, nil) +} + +// SetContacts starts a contact change of a domain, this will replace all existing contacts +func SetContacts(c gotransip.Client, domainName, contacts WhoisContacts) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setContacts", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("contacts", contacts) + + return c.Call(sr, nil) +} + +// GetAllTLDInfos returns slice with TLD objects or error when this failed +func GetAllTLDInfos(c gotransip.Client) ([]TLD, error) { + var d = struct { + TLD []TLD `xml:"item"` + }{} + err := c.Call(gotransip.SoapRequest{ + Service: serviceName, + Method: "getAllTldInfos", + }, &d) + + return d.TLD, err +} + +// GetTldInfo returns info about a specific TLD +func GetTldInfo(c gotransip.Client, tldName string) (TLD, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getTldInfo", + } + sr.AddArgument("tldName", tldName) + + var v TLD + err := c.Call(sr, &v) + return v, err +} + +// GetCurrentDomainAction returns info about the action this domain is currently running +func GetCurrentDomainAction(c gotransip.Client, domainName string) (ActionResult, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getCurrentDomainAction", + } + sr.AddArgument("domainName", domainName) + + var v ActionResult + err := c.Call(sr, &v) + return v, err +} + +// RetryCurrentDomainActionWithNewData retries a failed domain action with new +// domain data. The Domain.Name field must contain the name of the Domain. The +// Nameservers, Contacts, DNSEntries fields contain the new data for this domain. +func RetryCurrentDomainActionWithNewData(c gotransip.Client, domain Domain) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "retryCurrentDomainActionWithNewData", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} + +// RetryTransferWithDifferentAuthCode retries a transfer action with a new authcode +func RetryTransferWithDifferentAuthCode(c gotransip.Client, domain Domain, newAuthCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "retryTransferWithDifferentAuthCode", + } + sr.AddArgument("domain", domain) + sr.AddArgument("newAuthCode", newAuthCode) + + return c.Call(sr, nil) +} + +// CancelDomainAction cancels a failed domain action +func CancelDomainAction(c gotransip.Client, domain string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "cancelDomainAction", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} diff --git a/vendor/github.com/transip/gotransip/domain/domain.go b/vendor/github.com/transip/gotransip/domain/domain.go new file mode 100644 index 00000000..56d7b719 --- /dev/null +++ b/vendor/github.com/transip/gotransip/domain/domain.go @@ -0,0 +1,405 @@ +package domain + +import ( + "fmt" + "net" + + "github.com/transip/gotransip" + "github.com/transip/gotransip/util" +) + +const ( + serviceName string = "DomainService" +) + +// Domain represents a Transip_Domain object +// as described at https://api.transip.nl/docs/transip.nl/class-Transip_Domain.html +type Domain struct { + Name string `xml:"name"` + Nameservers []Nameserver `xml:"nameservers>item"` + Contacts []WhoisContact `xml:"contacts>item"` + DNSEntries []DNSEntry `xml:"dnsEntries>item"` + Branding Branding `xml:"branding"` + AuthorizationCode string `xml:"authCode"` + IsLocked bool `xml:"isLocked"` + RegistrationDate util.XMLTime `xml:"registrationDate"` + RenewalDate util.XMLTime `xml:"renewalDate"` +} + +// EncodeParams returns Domain parameters ready to be used for constructing a signature +func (d Domain) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + prm.Add(fmt.Sprintf("%d[name]", idx), d.Name) + prm.Add(fmt.Sprintf("%d[authCode]", idx), d.AuthorizationCode) + prm.Add(fmt.Sprintf("%d[isLocked]", idx), fmt.Sprintf("%t", d.IsLocked)) + prm.Add(fmt.Sprintf("%d[registrationDate]", idx), d.RegistrationDate.Format("2006-01-02")) + prm.Add(fmt.Sprintf("%d[renewalDate]", idx), d.RenewalDate.Format("2006-01-02")) + // nameservers + for i, e := range d.Nameservers { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + prm.Add(fmt.Sprintf("%d[nameservers][%d][hostname]", idx, i), e.Hostname) + prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv4]", idx, i), ipv4) + prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv6]", idx, i), ipv6) + } + // contacts + for i, e := range d.Contacts { + prm.Add(fmt.Sprintf("%d[contacts][%d][type]", idx, i), e.Type) + prm.Add(fmt.Sprintf("%d[contacts][%d][firstName]", idx, i), e.FirstName) + prm.Add(fmt.Sprintf("%d[contacts][%d][middleName]", idx, i), e.MiddleName) + prm.Add(fmt.Sprintf("%d[contacts][%d][lastName]", idx, i), e.LastName) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyName]", idx, i), e.CompanyName) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyKvk]", idx, i), e.CompanyKvk) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyType]", idx, i), e.CompanyType) + prm.Add(fmt.Sprintf("%d[contacts][%d][street]", idx, i), e.Street) + prm.Add(fmt.Sprintf("%d[contacts][%d][number]", idx, i), e.Number) + prm.Add(fmt.Sprintf("%d[contacts][%d][postalCode]", idx, i), e.PostalCode) + prm.Add(fmt.Sprintf("%d[contacts][%d][city]", idx, i), e.City) + prm.Add(fmt.Sprintf("%d[contacts][%d][phoneNumber]", idx, i), e.PhoneNumber) + prm.Add(fmt.Sprintf("%d[contacts][%d][faxNumber]", idx, i), e.FaxNumber) + prm.Add(fmt.Sprintf("%d[contacts][%d][email]", idx, i), e.Email) + prm.Add(fmt.Sprintf("%d[contacts][%d][country]", idx, i), e.Country) + } + // dnsEntries + for i, e := range d.DNSEntries { + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][name]", idx, i), e.Name) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL)) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][type]", idx, i), string(e.Type)) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][content]", idx, i), e.Content) + } + // branding + prm.Add(fmt.Sprintf("%d[branding][companyName]", idx), d.Branding.CompanyName) + prm.Add(fmt.Sprintf("%d[branding][supportEmail]", idx), d.Branding.SupportEmail) + prm.Add(fmt.Sprintf("%d[branding][companyUrl]", idx), d.Branding.CompanyURL) + prm.Add(fmt.Sprintf("%d[branding][termsOfUsageUrl]", idx), d.Branding.TermsOfUsageURL) + prm.Add(fmt.Sprintf("%d[branding][bannerLine1]", idx), d.Branding.BannerLine1) + prm.Add(fmt.Sprintf("%d[branding][bannerLine2]", idx), d.Branding.BannerLine2) + prm.Add(fmt.Sprintf("%d[branding][bannerLine3]", idx), d.Branding.BannerLine3) +} + +// EncodeArgs returns Domain XML body ready to be passed in the SOAP call +func (d Domain) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s xsi:type="ns1:Domain"> + %s + %s + %t + %s + %s`, + key, d.Name, d.AuthorizationCode, d.IsLocked, + d.RegistrationDate.Format("2006-01-02"), d.RenewalDate.Format("2006-01-02"), + ) + "\n" + + output += Nameservers(d.Nameservers).EncodeArgs("nameservers") + "\n" + output += WhoisContacts(d.Contacts).EncodeArgs("contacts") + "\n" + output += DNSEntries(d.DNSEntries).EncodeArgs("dnsEntries") + "\n" + output += d.Branding.EncodeArgs("branding") + "\n" + + return fmt.Sprintf("%s", output, key) +} + +// Capability represents the possible capabilities a TLD can have +type Capability string + +var ( + // CapabilityRequiresAuthCode defines this TLD requires an auth code + // to be transferred + CapabilityRequiresAuthCode Capability = "requiresAuthCode" + // CapabilityCanRegister defines this TLD can be registered + CapabilityCanRegister Capability = "canRegister" + // CapabilityCanTransferWithOwnerChange defines this TLD can be transferred + // with change of ownership + CapabilityCanTransferWithOwnerChange Capability = "canTransferWithOwnerChange" + // CapabilityCanTransferWithoutOwnerChange defines this TLD can be + // transferred without change of ownership + CapabilityCanTransferWithoutOwnerChange Capability = "canTransferWithoutOwnerChange" + // CapabilityCanSetLock defines this TLD allows to be locked + CapabilityCanSetLock Capability = "canSetLock" + // CapabilityCanSetOwner defines this TLD supports setting an owner + CapabilityCanSetOwner Capability = "canSetOwner" + // CapabilityCanSetContacts defines this TLD supports setting contacts + CapabilityCanSetContacts Capability = "canSetContacts" + // CapabilityCanSetNameservers defines this TLD supports setting nameservers + CapabilityCanSetNameservers Capability = "canSetNameservers" +) + +// TLD represents a Transip_Tld object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_Tld.html +type TLD struct { + Name string `xml:"name"` + Price float64 `xml:"price"` + RenewalPrice float64 `xml:"renewalPrice"` + Capabilities []Capability `xml:"capabilities>item"` + RegistrationPeriodLength int64 `xml:"registrationPeriodLength"` + CancelTimeFrame int64 `xml:"cancelTimeFrame"` +} + +// Nameserver represents a Transip_Nameserver object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_Nameserver.html +type Nameserver struct { + Hostname string `xml:"hostname"` + IPv4Address net.IP `xml:"ipv4"` + IPv6Address net.IP `xml:"ipv6"` +} + +// Nameservers is just an array of Nameserver +// basically only here so it can implement paramsEncoder +type Nameservers []Nameserver + +// EncodeParams returns Nameservers parameters ready to be used for constructing a signature +func (n Nameservers) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range n { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + prm.Add(fmt.Sprintf("%d[%d][hostname]", idx, i), e.Hostname) + prm.Add(fmt.Sprintf("%d[%d][ipv4]", idx, i), ipv4) + prm.Add(fmt.Sprintf("%d[%d][ipv6]", idx, i), ipv6) + } +} + +// EncodeArgs returns Nameservers XML body ready to be passed in the SOAP call +func (n Nameservers) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:Nameserver[%d]" xsi:type="ns1:ArrayOfNameserver">`, key, len(n)) + "\n" + for _, e := range n { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + output += fmt.Sprintf(` + %s + %s + %s + `, e.Hostname, ipv4, ipv6) + "\n" + } + + return fmt.Sprintf("%s", output, key) +} + +// DNSEntryType represents the possible types of DNS entries +type DNSEntryType string + +var ( + // DNSEntryTypeA represents an A-record + DNSEntryTypeA DNSEntryType = "A" + // DNSEntryTypeAAAA represents an AAAA-record + DNSEntryTypeAAAA DNSEntryType = "AAAA" + // DNSEntryTypeCNAME represents a CNAME-record + DNSEntryTypeCNAME DNSEntryType = "CNAME" + // DNSEntryTypeMX represents an MX-record + DNSEntryTypeMX DNSEntryType = "MX" + // DNSEntryTypeNS represents an NS-record + DNSEntryTypeNS DNSEntryType = "NS" + // DNSEntryTypeTXT represents a TXT-record + DNSEntryTypeTXT DNSEntryType = "TXT" + // DNSEntryTypeSRV represents an SRV-record + DNSEntryTypeSRV DNSEntryType = "SRV" +) + +// DNSEntry represents a Transip_DnsEntry object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DnsEntry.html +type DNSEntry struct { + Name string `xml:"name"` + TTL int64 `xml:"expire"` + Type DNSEntryType `xml:"type"` + Content string `xml:"content"` +} + +// DNSEntries is just an array of DNSEntry +// basically only here so it can implement paramsEncoder +type DNSEntries []DNSEntry + +// EncodeParams returns DNSEntries parameters ready to be used for constructing a signature +func (d DNSEntries) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range d { + prm.Add(fmt.Sprintf("%d[%d][name]", idx, i), e.Name) + prm.Add(fmt.Sprintf("%d[%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL)) + prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), string(e.Type)) + prm.Add(fmt.Sprintf("%d[%d][content]", idx, i), e.Content) + } +} + +// EncodeArgs returns DNSEntries XML body ready to be passed in the SOAP call +func (d DNSEntries) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:DnsEntry[%d]" xsi:type="ns1:ArrayOfDnsEntry">`, key, len(d)) + "\n" + for _, e := range d { + output += fmt.Sprintf(` + %s + %d + %s + %s + `, e.Name, e.TTL, e.Type, e.Content) + "\n" + } + + return fmt.Sprintf("%s", output, key) +} + +// Status reflects the current status of a domain in a check result +type Status string + +var ( + // StatusInYourAccount means he domain name is already in your account + StatusInYourAccount Status = "inyouraccount" + // StatusUnavailable means the domain name is currently unavailable and can not be registered due to unknown reasons. + StatusUnavailable Status = "unavailable" + // StatusNotFree means the domain name has already been registered + StatusNotFree Status = "notfree" + // StatusFree means the domain name is currently free, is available and can be registered + StatusFree Status = "free" + // StatusInternalPull means the domain name is currently registered at TransIP and is available to be pulled from another account to yours. + StatusInternalPull Status = "internalpull" + // StatusInternalPush means the domain name is currently registered at TransIP in your accounta and is available to be pushed to another account. + StatusInternalPush Status = "internalpush" +) + +// CheckResult represents a Transip_DomainCheckResult object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainCheckResult.html +type CheckResult struct { + DomainName string `xml:"domainName"` + Status Status `xml:"status"` + Actions []Action `xml:"actions>item"` +} + +// Branding represents a Transip_DomainBranding object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainBranding.html +type Branding struct { + CompanyName string `xml:"companyName"` + SupportEmail string `xml:"supportEmail"` + CompanyURL string `xml:"companyUrl"` + TermsOfUsageURL string `xml:"termsOfUsageUrl"` + BannerLine1 string `xml:"bannerLine1"` + BannerLine2 string `xml:"bannerLine2"` + BannerLine3 string `xml:"bannerLine3"` +} + +// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature +func (b Branding) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + prm.Add(fmt.Sprintf("%d[companyName]", idx), b.CompanyName) + prm.Add(fmt.Sprintf("%d[supportEmail]", idx), b.SupportEmail) + prm.Add(fmt.Sprintf("%d[companyUrl]", idx), b.CompanyURL) + prm.Add(fmt.Sprintf("%d[termsOfUsageUrl]", idx), b.TermsOfUsageURL) + prm.Add(fmt.Sprintf("%d[bannerLine1]", idx), b.BannerLine1) + prm.Add(fmt.Sprintf("%d[bannerLine2]", idx), b.BannerLine2) + prm.Add(fmt.Sprintf("%d[bannerLine3]", idx), b.BannerLine3) +} + +// EncodeArgs returns Branding XML body ready to be passed in the SOAP call +func (b Branding) EncodeArgs(key string) string { + return fmt.Sprintf(` + %s + %s + %s + %s + %s + %s + %s + `, b.CompanyName, b.SupportEmail, b.CompanyURL, b.TermsOfUsageURL, b.BannerLine1, b.BannerLine2, b.BannerLine3) +} + +// Action reflects the available actions to perform on a domain +type Action string + +var ( + // ActionRegister registers a domain + ActionRegister Action = "register" + // ActionTransfer transfers a domain to another provider + ActionTransfer Action = "transfer" + // ActionInternalPull transfers a domain to another account at TransIP + ActionInternalPull Action = "internalpull" +) + +// ActionResult represents a Transip_DomainAction object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainAction.html +type ActionResult struct { + Name string `xml:"name"` + HasFailed bool `xml:"hasFailed"` + Message string `xml:"message"` +} + +// WhoisContact represents a TransIP_WhoisContact object +// as described at https://api.transip.nl/docs/transip.nl/class-Transip_WhoisContact.html +type WhoisContact struct { + Type string `xml:"type"` + FirstName string `xml:"firstName"` + MiddleName string `xml:"middleName"` + LastName string `xml:"lastName"` + CompanyName string `xml:"companyName"` + CompanyKvk string `xml:"companyKvk"` + CompanyType string `xml:"companyType"` + Street string `xml:"street"` + Number string `xml:"number"` + PostalCode string `xml:"postalCode"` + City string `xml:"city"` + PhoneNumber string `xml:"phoneNumber"` + FaxNumber string `xml:"faxNumber"` + Email string `xml:"email"` + Country string `xml:"country"` +} + +// WhoisContacts is just an array of WhoisContact +// basically only here so it can implement paramsEncoder +type WhoisContacts []WhoisContact + +// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature +func (w WhoisContacts) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range w { + prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), e.Type) + prm.Add(fmt.Sprintf("%d[%d][firstName]", idx, i), e.FirstName) + prm.Add(fmt.Sprintf("%d[%d][middleName]", idx, i), e.MiddleName) + prm.Add(fmt.Sprintf("%d[%d][lastName]", idx, i), e.LastName) + prm.Add(fmt.Sprintf("%d[%d][companyName]", idx, i), e.CompanyName) + prm.Add(fmt.Sprintf("%d[%d][companyKvk]", idx, i), e.CompanyKvk) + prm.Add(fmt.Sprintf("%d[%d][companyType]", idx, i), e.CompanyType) + prm.Add(fmt.Sprintf("%d[%d][street]", idx, i), e.Street) + prm.Add(fmt.Sprintf("%d[%d][number]", idx, i), e.Number) + prm.Add(fmt.Sprintf("%d[%d][postalCode]", idx, i), e.PostalCode) + prm.Add(fmt.Sprintf("%d[%d][city]", idx, i), e.City) + prm.Add(fmt.Sprintf("%d[%d][phoneNumber]", idx, i), e.PhoneNumber) + prm.Add(fmt.Sprintf("%d[%d][faxNumber]", idx, i), e.FaxNumber) + prm.Add(fmt.Sprintf("%d[%d][email]", idx, i), e.Email) + prm.Add(fmt.Sprintf("%d[%d][country]", idx, i), e.Country) + } +} + +// EncodeArgs returns WhoisContacts XML body ready to be passed in the SOAP call +func (w WhoisContacts) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:WhoisContact[%d]" xsi:type="ns1:ArrayOfWhoisContact">`, key, len(w)) + "\n" + for _, e := range w { + output += fmt.Sprintf(` + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + `, e.Type, e.FirstName, e.MiddleName, e.LastName, e.CompanyName, + e.CompanyKvk, e.CompanyType, e.Street, e.Number, e.PostalCode, e.City, + e.PhoneNumber, e.FaxNumber, e.Email, e.Country) + "\n" + } + + return output + fmt.Sprintf("", key) +} diff --git a/vendor/github.com/transip/gotransip/sign.go b/vendor/github.com/transip/gotransip/sign.go new file mode 100644 index 00000000..8b7d360c --- /dev/null +++ b/vendor/github.com/transip/gotransip/sign.go @@ -0,0 +1,49 @@ +package gotransip + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "net/url" +) + +var ( + asn1Header = []byte{ + 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40, + } +) + +func signWithKey(params *soapParams, key []byte) (string, error) { + // create SHA512 hash of given parameters + h := sha512.New() + h.Write([]byte(params.Encode())) + + // prefix ASN1 header to SHA512 hash + digest := append(asn1Header, h.Sum(nil)...) + + // prepare key struct + block, _ := pem.Decode(key) + if block == nil { + return "", errors.New("could not decode private key") + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("could not parse private key: %s", err.Error()) + } + + pkey := parsed.(*rsa.PrivateKey) + + enc, err := rsa.SignPKCS1v15(rand.Reader, pkey, crypto.Hash(0), digest) + if err != nil { + return "", fmt.Errorf("could not sign data: %s", err.Error()) + } + + return url.QueryEscape(base64.StdEncoding.EncodeToString(enc)), nil +} diff --git a/vendor/github.com/transip/gotransip/soap.go b/vendor/github.com/transip/gotransip/soap.go new file mode 100644 index 00000000..b893b7f9 --- /dev/null +++ b/vendor/github.com/transip/gotransip/soap.go @@ -0,0 +1,419 @@ +package gotransip + +import ( + "bytes" + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +const ( + // format for SOAP envelopes + soapEnvelopeFixture string = ` + + %s +` +) + +// getSOAPArgs returns XML representing given name and argument as SOAP body +func getSOAPArgs(name string, input ...string) []byte { + var buf bytes.Buffer + + buf.WriteString(fmt.Sprintf("", name)) + for _, x := range input { + buf.WriteString(x) + } + buf.WriteString(fmt.Sprintf("", name)) + + return buf.Bytes() +} + +// getSOAPArg returns XML representing given input argument as SOAP parameters +// in combination with getSOAPArgs you can build SOAP body +func getSOAPArg(name string, input interface{}) (output string) { + switch input.(type) { + case []string: + i := input.([]string) + output = fmt.Sprintf(`<%s SOAP-ENC:arrayType="xsd:string[%d]" xsi:type="ns1:ArrayOfString">`, name, len(i)) + for _, x := range i { + output = output + fmt.Sprintf(`%s`, x) + } + output = output + fmt.Sprintf("", name) + case string: + output = fmt.Sprintf(`<%s xsi:type="xsd:string">%s`, name, input, name) + case int, int32, int64: + output = fmt.Sprintf(`<%s xsi:type="xsd:integer">%d`, name, input, name) + } + + return +} + +type soapFault struct { + Code string `xml:"faultcode,omitempty"` + Description string `xml:"faultstring,omitempty"` +} + +func (s soapFault) String() string { + return fmt.Sprintf("SOAP Fault %s: %s", s.Code, s.Description) +} + +// paramsEncoder allows SoapParams to hook into encoding theirselves, useful when +// fields consist of complex structs +type paramsEncoder interface { + EncodeParams(ParamsContainer) + EncodeArgs(string) string +} + +// ParamsContainer is the interface a type should implement to be able to hold +// SOAP parameters +type ParamsContainer interface { + Len() int + Add(string, interface{}) +} + +// soapParams is a utility to make sure parameter data is encoded into a query +// in the same order as we set them. The TransIP API requires this order for +// verifying the signature +type soapParams struct { + keys []string + values []interface{} +} + +// Add adds parameter data to the end of this SoapParams +func (s *soapParams) Add(k string, v interface{}) { + if s.keys == nil { + s.keys = make([]string, 0) + } + + if s.values == nil { + s.values = make([]interface{}, 0) + } + + s.keys = append(s.keys, k) + s.values = append(s.values, v) +} + +// Len returns amount of parameters set in this SoapParams +func (s soapParams) Len() int { + return len(s.keys) +} + +// Encode returns a URL-like query string that can be used to generate a request's +// signature. It's similar to url.Values.Encode() but without sorting of the keys +// and based on the value's type it tries to encode accordingly. +func (s soapParams) Encode() string { + var buf bytes.Buffer + var key string + + for i, v := range s.values { + // if this is not the first parameter, prefix with & + if i > 0 { + buf.WriteString("&") + } + + // for empty data fields, don't encode anything + if v == nil { + continue + } + + key = s.keys[i] + + switch v.(type) { + case []string: + c := v.([]string) + for j, cc := range c { + if j > 0 { + buf.WriteString("&") + } + buf.WriteString(fmt.Sprintf("%s[%d]=", key, j)) + buf.WriteString(strings.Replace(url.QueryEscape(cc), "+", "%20", -1)) + } + case string: + c := v.(string) + buf.WriteString(fmt.Sprintf("%s=", key)) + buf.WriteString(strings.Replace(url.QueryEscape(c), "+", "%20", -1)) + case int, int8, int16, int32, int64: + buf.WriteString(fmt.Sprintf("%s=", key)) + buf.WriteString(fmt.Sprintf("%d", v)) + default: + continue + } + } + + return buf.String() +} + +type soapHeader struct { + XMLName struct{} `xml:"Header"` + Contents []byte `xml:",innerxml"` +} + +type soapBody struct { + XMLName struct{} `xml:"Body"` + Contents []byte `xml:",innerxml"` +} + +type soapResponse struct { + Response struct { + InnerXML []byte `xml:",innerxml"` + } `xml:"return"` +} + +type soapEnvelope struct { + XMLName struct{} `xml:"Envelope"` + Header soapHeader + Body soapBody +} + +// SoapRequest holds all information for perfoming a SOAP request +// Arguments to the request can be specified with AddArgument +// If padding is defined, the SOAP response will be parsed after it being padded +// with items in Padding in reverse order +type SoapRequest struct { + Service string + Method string + params *soapParams // params used for creating signature + args []string // XML body arguments + Padding []string +} + +// AddArgument adds an argument to the SoapRequest; the arguments ared used to +// fill the XML request body as well as to create a valid signature for the +// request +func (sr *SoapRequest) AddArgument(key string, value interface{}) { + if sr.params == nil { + sr.params = &soapParams{} + } + + // check if value implements paramsEncoder + if pe, ok := value.(paramsEncoder); ok { + sr.args = append(sr.args, pe.EncodeArgs(key)) + pe.EncodeParams(sr.params) + return + } + + switch value.(type) { + case []string: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, value)) + case string: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, value.(string))) + case int, int8, int16, int32, int64: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, fmt.Sprintf("%d", value))) + default: + // check if value implements the String interface + if str, ok := value.(fmt.Stringer); ok { + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), str.String()) + sr.args = append(sr.args, getSOAPArg(key, str.String())) + } + } +} + +func (sr SoapRequest) getEnvelope() string { + return fmt.Sprintf(soapEnvelopeFixture, transipAPIHost, getSOAPArgs(sr.Method, sr.args...)) +} + +type soapClient struct { + Login string + Mode APIMode + PrivateKey []byte +} + +// httpReqForSoapRequest creates the HTTP request for a specific SoapRequest +// this includes setting the URL, POST body and cookies +func (s soapClient) httpReqForSoapRequest(req SoapRequest) (*http.Request, error) { + // format URL + url := fmt.Sprintf("https://%s/soap/?service=%s", transipAPIHost, req.Service) + + // create HTTP request + // TransIP API SOAP requests are always POST requests + httpReq, err := http.NewRequest("POST", url, strings.NewReader(req.getEnvelope())) + if err != nil { + return nil, err + } + + // generate a number-used-once, a.k.a. nonce + // seeding the RNG is important if we want to do prevent using the same nonce + // in 2 sequential requests + rand.Seed(time.Now().UnixNano()) + nonce := fmt.Sprintf("%d", rand.Int()) + // set time of request, used later for signature as well + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + // set cookies required for the request + // most of these cookies are used for the signature as well so they should + // obviously match + httpReq.AddCookie(&http.Cookie{ + Name: "login", + Value: s.Login, + }) + httpReq.AddCookie(&http.Cookie{ + Name: "mode", + Value: string(s.Mode), + }) + httpReq.AddCookie(&http.Cookie{ + Name: "timestamp", + Value: timestamp, + }) + httpReq.AddCookie(&http.Cookie{ + Name: "nonce", + Value: nonce, + }) + + // add params required for signature to the request parameters + if req.params == nil { + req.params = &soapParams{} + } + // TransIP API is quite picky on the order of the parameters + // so don't change anything in the order below + req.params.Add("__method", req.Method) + req.params.Add("__service", req.Service) + req.params.Add("__hostname", transipAPIHost) + req.params.Add("__timestamp", timestamp) + req.params.Add("__nonce", nonce) + + signature, err := signWithKey(req.params, s.PrivateKey) + if err != nil { + return nil, err + } + + // add signature of the request to the cookies as well + httpReq.AddCookie(&http.Cookie{ + Name: "signature", + Value: signature, + }) + + return httpReq, nil +} + +func parseSoapResponse(data []byte, padding []string, statusCode int, result interface{}) error { + // try to decode the resulting XML + var env soapEnvelope + if err := xml.Unmarshal(data, &env); err != nil { + return err + } + + // try to decode the body to a soapFault + var fault soapFault + if err := xml.Unmarshal(env.Body.Contents, &fault); err != nil { + return err + } + + // by checking fault's Code, we can determine if the response body in fact + // was a SOAP fault and if it was: return it as an error + if len(fault.Code) > 0 { + return errors.New(fault.String()) + } + + // try to decode into soapResponse + sr := soapResponse{} + if err := xml.Unmarshal(env.Body.Contents, &sr); err != nil { + return err + } + + // if the response was empty and HTTP status was 200, consider it a success + if len(sr.Response.InnerXML) == 0 && statusCode == 200 { + return nil + } + + // it seems like xml.Unmarshal won't work well on the most outer element + // so even when no Padding is defined, always pad with "transip" element + p := append([]string{"transip"}, padding...) + innerXML := padXMLData(sr.Response.InnerXML, p) + + // try to decode to given result interface + return xml.Unmarshal([]byte(innerXML), &result) +} + +func (s *soapClient) call(req SoapRequest, result interface{}) error { + // get http request for soap request + httpReq, err := s.httpReqForSoapRequest(req) + if err != nil { + return err + } + + // create HTTP client and do the actual request + client := &http.Client{Timeout: time.Second * 10} + // make sure to verify the validity of remote certificate + // this is the default, but adding this flag here makes it easier to toggle + // it for testing/debugging + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("request error:\n%s", err.Error()) + } + defer resp.Body.Close() + + // read entire response body + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // parse SOAP response into given result interface + return parseSoapResponse(b, req.Padding, resp.StatusCode, result) +} + +// apply given padding around the XML data fed into this function +// padding is applied in reverse order, so last element of padding is the +// innermost element in the resulting XML +func padXMLData(data []byte, padding []string) []byte { + // get right information from padding elements by matching to regex + re, _ := regexp.Compile("^]+)>?$") + + var prefix, suffix []byte + var tag, attr string + // go over each padding element + for i := len(padding); i > 0; i-- { + res := re.FindStringSubmatch(padding[i-1]) + // no attribute was given + if len(res[1]) == 0 { + tag = res[2] + attr = "" + } else { + tag = res[1] + attr = " " + res[2] + } + + prefix = []byte(fmt.Sprintf("<%s%s>", tag, attr)) + suffix = []byte(fmt.Sprintf("", tag)) + data = append(append(prefix, data...), suffix...) + } + + return data +} + +// TestParamsContainer is only useful for unit testing the ParamsContainer +// implementation of other type +type TestParamsContainer struct { + Prm string +} + +// Add just makes sure we use Len(), key and value in the result so it can be +// tested +func (t *TestParamsContainer) Add(key string, value interface{}) { + var prefix string + if t.Len() > 0 { + prefix = "&" + } + t.Prm = t.Prm + prefix + fmt.Sprintf("%d%s=%s", t.Len(), key, value) +} + +// Len returns current length of test data in TestParamsContainer +func (t TestParamsContainer) Len() int { + return len(t.Prm) +} diff --git a/vendor/github.com/transip/gotransip/util/util.go b/vendor/github.com/transip/gotransip/util/util.go new file mode 100644 index 00000000..7f1beecf --- /dev/null +++ b/vendor/github.com/transip/gotransip/util/util.go @@ -0,0 +1,37 @@ +package util + +import ( + "encoding/xml" + "time" +) + +// KeyValueXML resembles the complex struct for getting key/value pairs from XML +type KeyValueXML struct { + Cont []struct { + Item []struct { + Key string `xml:"key"` + Value string `xml:"value"` + } `xml:"item"` + } `xml:"item"` +} + +// XMLTime is a custom type to decode XML values to time.Time directly +type XMLTime struct { + time.Time +} + +// UnmarshalXML is implemented to be able act as custom XML type +// it tries to parse time from given elements value +func (x *XMLTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + if err := d.DecodeElement(&v, &start); err != nil { + return err + } + + if p, _ := time.Parse("2006-01-02 15:04:05", v); !p.IsZero() { + *x = XMLTime{p} + } else if p, _ := time.Parse("2006-01-02", v); !p.IsZero() { + *x = XMLTime{p} + } + return nil +}