420 lines
11 KiB
Go
420 lines
11 KiB
Go
|
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)
|
||
|
}
|