blob: 2428712ff8bfaf635b4004cb35b37d1b6f496276 [file]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package xmlrpc implements the XML-RPC client library.
package xmlrpc
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io/ioutil"
"math"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
const defaultRPCTimeout = 10 * time.Second
// maxRPCTimeout is very long, because some of the servo calls are really slow
const maxRPCTimeout = 90 * time.Second
// XMLRpc holds the XML-RPC information.
type XMLRpc struct {
host string
port int
}
// New creates a new XMLRpc object for communicating with XML-RPC server.
func New(host string, port int) *XMLRpc {
return &XMLRpc{host: host, port: port}
}
// Call represents a XML-RPC call request.
type Call struct {
method string
args []interface{}
timeout time.Duration
}
// methodCall mirrors the structure of an XML-RPC method call.
type methodCall struct {
XMLName xml.Name `xml:"methodCall"`
MethodName string `xml:"methodName"`
Params *[]param `xml:"params>param"`
}
// methodResponse is an XML-RPC response.
type methodResponse struct {
XMLName xml.Name `xml:"methodResponse"`
Params *[]param `xml:"params>param,omitempty"`
Fault *fault `xml:"fault,omitempty"`
}
// param is an XML-RPC param.
type param struct {
Value value `xml:"value"`
}
// fault is an XML-RPC fault.
// If present, it usually contains in its value a struct of two members:
// faultCode (an int) and faultString (a string).
type fault struct {
Value value `xml:"value"`
}
// value is an XML-RPC value.
type value struct {
Boolean *string `xml:"boolean,omitempty"`
Double *string `xml:"double,omitempty"`
Int *string `xml:"int,omitempty"`
Str *string `xml:"string,omitempty"`
Array *xmlArray `xml:"array,omitempty"`
Struct *xmlStruct `xml:"struct,omitempty"`
}
// xmlArray is an XML-RPC array.
type xmlArray struct {
Values []value `xml:"data>value,omitempty"`
}
// xmlStruct is an XML-RPC struct.
type xmlStruct struct {
Members []member `xml:"member,omitempty"`
}
// member is an XML-RPC object containing a name and a value.
type member struct {
Name string `xml:"name"`
Value value `xml:"value"`
}
// String implements the String() interface of value.
func (v value) String() string {
if v.Boolean != nil {
return "(boolean)" + *v.Boolean
}
if v.Double != nil {
return "(double)" + *v.Double
}
if v.Int != nil {
return "(int)" + *v.Int
}
if v.Str != nil {
return "(string)" + *v.Str
}
if v.Array != nil {
var values []string
for _, e := range v.Array.Values {
values = append(values, e.String())
}
return "[" + strings.Join(values, ", ") + "]"
}
if v.Struct != nil {
var values []string
for _, m := range v.Struct.Members {
values = append(values, fmt.Sprintf("%s: %s", m.Name, m.Value.String()))
}
return "{" + strings.Join(values, ", ") + "}"
}
return "<empty>"
}
// FaultError is a type of error representing an XML-RPC fault.
type FaultError struct {
Code int
Reason string
// Including *errors.E allows FaultError to work with the Tast errors library.
*errors.E
}
// NewFaultError creates a FaultError.
func NewFaultError(code int, reason string) FaultError {
return FaultError{
Code: code,
Reason: reason,
E: errors.Errorf("xml-rpc fault with code %d: %s", code, reason),
}
}
// xmlBooleanToBool converts the strings '1' or '0' into boolean.
func xmlBooleanToBool(xmlBool string) (bool, error) {
if len(xmlBool) != 1 {
return false, errors.Errorf("xmlBooleanToBool got %q; expected '1' or '0'", xmlBool)
}
switch xmlBool[0] {
case '1':
return true, nil
case '0':
return false, nil
default:
return false, errors.Errorf("xmlBooleanToBool got %q; expected '1' or '0'", xmlBool)
}
}
// boolToXMLBoolean converts a Go boolean to an XML-RPC boolean string.
func boolToXMLBoolean(v bool) string {
if v {
return "1"
}
return "0"
}
// xmlIntegerToInt converts numeric strings such as '-1' into integers.
func xmlIntegerToInt(xmlInt string) (int, error) {
if len(xmlInt) == 0 {
return 0, errors.New("xmlIntegerToInt got empty xml value")
}
i, err := strconv.ParseInt(xmlInt, 10, 32)
if err != nil {
return 0, err
}
return int(i), nil
}
// intToXMLInteger converts a Go integer to an XML-RPC integer string.
func intToXMLInteger(i int) (string, error) {
if i > math.MaxInt32 || i < math.MinInt32 {
return "", errors.Errorf("intToXMLInteger needs a value that can fit in an int32: got %d, want between %d and %d", i, math.MinInt32, math.MaxInt32)
}
return strconv.FormatInt(int64(i), 10), nil
}
// xmlDoubleToFloat64 converts double-like strings such as "1.5" into float64s.
func xmlDoubleToFloat64(s string) (float64, error) {
if len(s) == 0 {
return 0.0, errors.New("xmlDoubleToFloat64 got empty xml value")
}
return strconv.ParseFloat(s, 64)
}
// float64ToXMLDouble converts a Go float64 to an XML-RPC double-like string.
func float64ToXMLDouble(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
// newValue creates an XML-RPC <value>.
func newValue(in interface{}) (value, error) {
// TODO(crbug.com/1201727): Support more data types, such as Golang map to XML-RPC struct.
if reflect.TypeOf(in).Kind() == reflect.Slice || reflect.TypeOf(in).Kind() == reflect.Array {
v := reflect.ValueOf(in)
var a xmlArray
for i := 0; i < v.Len(); i++ {
val, err := newValue(v.Index(i).Interface())
if err != nil {
return value{}, err
}
a.Values = append(a.Values, val)
}
return value{Array: &a}, nil
}
switch v := in.(type) {
case string:
s := v
return value{Str: &s}, nil
case bool:
b := boolToXMLBoolean(v)
return value{Boolean: &b}, nil
case int:
i, err := intToXMLInteger(v)
if err != nil {
return value{}, err
}
return value{Int: &i}, nil
case float64:
f := float64ToXMLDouble(v)
return value{Double: &f}, nil
case map[string]string:
var s xmlStruct
for key, obj := range v {
str := obj
s.Members = append(s.Members, member{Name: key, Value: value{Str: &str}})
}
return value{Struct: &s}, nil
}
return value{}, errors.Errorf("%q is not a supported type for newValue", reflect.TypeOf(in))
}
// newParams creates a list of XML-RPC <params>.
func newParams(args []interface{}) ([]param, error) {
var params []param
for _, arg := range args {
v, err := newValue(arg)
if err != nil {
return nil, err
}
params = append(params, param{v})
}
return params, nil
}
// NewCall creates a XML-RPC call.
func NewCall(method string, args ...interface{}) Call {
return Call{method, args, defaultRPCTimeout}
}
// NewCallTimeout creates a XML-RPC call.
func NewCallTimeout(method string, timeout time.Duration, args ...interface{}) Call {
return Call{method, args, timeout}
}
// serializeMethodCall turns a method and args into a serialized XML-RPC method call.
func serializeMethodCall(cl Call) ([]byte, error) {
params, err := newParams(cl.args)
if err != nil {
return nil, err
}
return xml.Marshal(&methodCall{MethodName: cl.method, Params: &params})
}
// getTimeout returns the lowest of the default timeout or remaining duration
// to the context's deadline.
func getTimeout(ctx context.Context, cl Call) time.Duration {
timeout := cl.timeout
if timeout > maxRPCTimeout {
timeout = maxRPCTimeout
testing.ContextLog(ctx, "Using max timeout ", timeout)
}
if dl, ok := ctx.Deadline(); ok {
newTimeout := dl.Sub(time.Now())
if newTimeout < timeout {
timeout = newTimeout
}
}
return timeout
}
// unpackValue unpacks a value struct into the given pointers.
func unpackValue(val value, out interface{}) error {
//TODO(crbug.com/1201727): Support unpack more data types, such as XML-RPC struct.
switch o := out.(type) {
case *string:
if val.Str == nil {
return errors.Errorf("value %s is not a string value", val)
}
*o = *val.Str
case *bool:
if val.Boolean == nil {
return errors.Errorf("value %s is not a boolean value", val)
}
v, err := xmlBooleanToBool(*val.Boolean)
if err != nil {
return err
}
*o = v
case *int:
if val.Int == nil {
return errors.Errorf("value %s is not an int value", val)
}
i, err := xmlIntegerToInt(*val.Int)
if err != nil {
return err
}
*o = i
case *float64:
if val.Double == nil {
return errors.Errorf("value %s is not a double value", val)
}
f, err := xmlDoubleToFloat64(*val.Double)
if err != nil {
return err
}
*o = f
case *[]string:
if val.Array == nil {
return errors.Errorf("value %s is not an array value", val)
}
for _, e := range val.Array.Values {
var i string
if err := unpackValue(e, &i); err != nil {
return err
}
*o = append(*o, i)
}
case *[]bool:
if val.Array == nil {
return errors.Errorf("value %s is not an array value", val)
}
for _, e := range val.Array.Values {
var i bool
if err := unpackValue(e, &i); err != nil {
return err
}
*o = append(*o, i)
}
case *[]int:
if val.Array == nil {
return errors.Errorf("value %s is not an array value", val)
}
for _, e := range val.Array.Values {
var i int
if err := unpackValue(e, &i); err != nil {
return err
}
*o = append(*o, i)
}
case *[]float64:
if val.Array == nil {
return errors.Errorf("value %s is not an array value", val)
}
for _, e := range val.Array.Values {
var i float64
if err := unpackValue(e, &i); err != nil {
return err
}
*o = append(*o, i)
}
case *map[string]string:
if val.Struct == nil {
return errors.Errorf("value %s is not a map", val)
}
for _, e := range val.Struct.Members {
var i string
if err := unpackValue(e.Value, &i); err != nil {
return err
}
(*o)[e.Name] = i
}
default:
return errors.Errorf("%q is not a supported type for unpackValue", reflect.TypeOf(out))
}
return nil
}
// unpack extracts a response's arguments into a list of given pointers.
func (r *methodResponse) unpack(out []interface{}) error {
if r.Params == nil {
if len(out) != 0 {
return errors.Errorf("response contains no args; want %d", len(out))
}
return nil
}
if len(*r.Params) != len(out) {
return errors.Errorf("response contains %d arg(s); want %d", len(*r.Params), len(out))
}
for i, p := range *r.Params {
if err := unpackValue(p.Value, out[i]); err != nil {
return errors.Wrapf(err, "failed to unpack response param at index %d", i)
}
}
return nil
}
// checkFault returns a FaultError if the response contains a fault with a non-zero faultCode.
func (r *methodResponse) checkFault() error {
if r.Fault == nil {
return nil
}
if r.Fault.Value.Struct == nil {
return errors.Errorf("fault %s doesn't contain xml-rpc struct", r.Fault.Value)
}
var rawFaultCode string
var faultString string
for _, m := range r.Fault.Value.Struct.Members {
switch m.Name {
case "faultCode":
if m.Value.Int == nil {
return errors.Errorf("faultCode %s doesn't provide integer value", m.Value)
}
rawFaultCode = *m.Value.Int
case "faultString":
if m.Value.Str == nil {
return errors.Errorf("faultString %s doesn't provide string value", m.Value)
}
faultString = *m.Value.Str
default:
return errors.Errorf("unexpected fault member name: %s", m.Name)
}
}
faultCode, err := xmlIntegerToInt(rawFaultCode)
if err != nil {
return errors.Wrap(err, "interpreting fault code")
}
if faultCode == 0 {
return errors.Errorf("response contained a fault with unexpected code 0; want non-0: faultString=%s", faultString)
}
return NewFaultError(faultCode, faultString)
}
// Run makes an XML-RPC call to the server.
func (r *XMLRpc) Run(ctx context.Context, cl Call, out ...interface{}) error {
body, err := serializeMethodCall(cl)
if err != nil {
return err
}
// Get RPC timeout duration from context or use default.
timeout := getTimeout(ctx, cl)
serverURL := fmt.Sprintf("http://%s:%d", r.host, r.port)
httpClient := &http.Client{Timeout: timeout}
resp, err := httpClient.Post(serverURL, "text/xml", bytes.NewBuffer(body))
if err != nil {
return errors.Wrapf(err, "timeout = %v", timeout)
}
defer resp.Body.Close()
// Read body and unmarshal XML.
bodyBytes, err := ioutil.ReadAll(resp.Body)
res := methodResponse{}
if err = xml.Unmarshal(bodyBytes, &res); err != nil {
return err
}
if err = res.checkFault(); err != nil {
return err
}
// If outs are specified, unpack response params.
// Otherwise, return without unpacking.
if len(out) > 0 {
if err := res.unpack(out); err != nil {
testing.ContextLogf(ctx, "Failed to unpack XML-RPC response for request %v: %s", cl, string(bodyBytes))
return err
}
}
return nil
}