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("%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)
}