1
0
mirror of https://github.com/go-acme/lego.git synced 2024-11-25 00:56:20 +02:00

Add DNS Provider for TransIP (#703)

This commit is contained in:
Maarten den Braber 2018-11-06 11:56:28 +01:00 committed by Ludovic Fernandez
parent 42d8637d87
commit 1837a3bb1c
14 changed files with 1711 additions and 0 deletions

14
Gopkg.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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":

View File

@ -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
}

View File

@ -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)
}

21
vendor/github.com/transip/gotransip/LICENSE generated vendored Normal file
View File

@ -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.

12
vendor/github.com/transip/gotransip/api.go generated vendored Normal file
View File

@ -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"
)

119
vendor/github.com/transip/gotransip/client.go generated vendored Normal file
View File

@ -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)
}

314
vendor/github.com/transip/gotransip/domain/api.go generated vendored Normal file
View File

@ -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)
}

405
vendor/github.com/transip/gotransip/domain/domain.go generated vendored Normal file
View File

@ -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">
<name xsi:type="xsd:string">%s</name>
<authCode xsi:type="xsd:string">%s</authCode>
<isLocked xsi:type="xsd:boolean">%t</isLocked>
<registrationDate xsi:type="xsd:string">%s</registrationDate>
<renewalDate xsi:type="xsd:string">%s</renewalDate>`,
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</%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(` <item xsi:type="ns1:Nameserver">
<hostname xsi:type="xsd:string">%s</hostname>
<ipv4 xsi:type="xsd:string">%s</ipv4>
<ipv6 xsi:type="xsd:string">%s</ipv6>
</item>`, e.Hostname, ipv4, ipv6) + "\n"
}
return fmt.Sprintf("%s</%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(` <item xsi:type="ns1:DnsEntry">
<name xsi:type="xsd:string">%s</name>
<expire xsi:type="xsd:int">%d</expire>
<type xsi:type="xsd:string">%s</type>
<content xsi:type="xsd:string">%s</content>
</item>`, e.Name, e.TTL, e.Type, e.Content) + "\n"
}
return fmt.Sprintf("%s</%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(`<branding xsi:type="ns1:DomainBranding">
<companyName xsi:type="xsd:string">%s</companyName>
<supportEmail xsi:type="xsd:string">%s</supportEmail>
<companyUrl xsi:type="xsd:string">%s</companyUrl>
<termsOfUsageUrl xsi:type="xsd:string">%s</termsOfUsageUrl>
<bannerLine1 xsi:type="xsd:string">%s</bannerLine1>
<bannerLine2 xsi:type="xsd:string">%s</bannerLine2>
<bannerLine3 xsi:type="xsd:string">%s</bannerLine3>
</branding>`, 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(` <item xsi:type="ns1:WhoisContact">
<type xsi:type="xsd:string">%s</type>
<firstName xsi:type="xsd:string">%s</firstName>
<middleName xsi:type="xsd:string">%s</middleName>
<lastName xsi:type="xsd:string">%s</lastName>
<companyName xsi:type="xsd:string">%s</companyName>
<companyKvk xsi:type="xsd:string">%s</companyKvk>
<companyType xsi:type="xsd:string">%s</companyType>
<street xsi:type="xsd:string">%s</street>
<number xsi:type="xsd:string">%s</number>
<postalCode xsi:type="xsd:string">%s</postalCode>
<city xsi:type="xsd:string">%s</city>
<phoneNumber xsi:type="xsd:string">%s</phoneNumber>
<faxNumber xsi:type="xsd:string">%s</faxNumber>
<email xsi:type="xsd:string">%s</email>
<country xsi:type="xsd:string">%s</country>
</item>`, 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("</%s>", key)
}

49
vendor/github.com/transip/gotransip/sign.go generated vendored Normal file
View File

@ -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
}

419
vendor/github.com/transip/gotransip/soap.go generated vendored Normal file
View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="%s">
<SOAP-ENV:Body>%s</SOAP-ENV:Body>
</SOAP-ENV:Envelope>`
)
// 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("<ns1:%s>", name))
for _, x := range input {
buf.WriteString(x)
}
buf.WriteString(fmt.Sprintf("</ns1:%s>", 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(`<item xsi:type="xsd:string">%s</item>`, x)
}
output = output + fmt.Sprintf("</%s>", name)
case string:
output = fmt.Sprintf(`<%s xsi:type="xsd:string">%s</%s>`, name, input, name)
case int, int32, int64:
output = fmt.Sprintf(`<%s xsi:type="xsd:integer">%d</%s>`, 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("</%s>", 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)
}

37
vendor/github.com/transip/gotransip/util/util.go generated vendored Normal file
View File

@ -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
}