client.go raw

   1  package xmlrpc
   2  
   3  import (
   4  	"errors"
   5  	"fmt"
   6  	"io/ioutil"
   7  	"net/http"
   8  	"net/http/cookiejar"
   9  	"net/rpc"
  10  	"net/url"
  11  	"sync"
  12  )
  13  
  14  type Client struct {
  15  	*rpc.Client
  16  }
  17  
  18  // clientCodec is rpc.ClientCodec interface implementation.
  19  type clientCodec struct {
  20  	// url presents url of xmlrpc service
  21  	url *url.URL
  22  
  23  	// httpClient works with HTTP protocol
  24  	httpClient *http.Client
  25  
  26  	// cookies stores cookies received on last request
  27  	cookies http.CookieJar
  28  
  29  	// responses presents map of active requests. It is required to return request id, that
  30  	// rpc.Client can mark them as done.
  31  	responses map[uint64]*http.Response
  32  	mutex     sync.Mutex
  33  
  34  	response Response
  35  
  36  	// ready presents channel, that is used to link request and it`s response.
  37  	ready chan uint64
  38  
  39  	// close notifies codec is closed.
  40  	close chan uint64
  41  }
  42  
  43  func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
  44  	httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
  45  
  46  	if err != nil {
  47  		return err
  48  	}
  49  
  50  	if codec.cookies != nil {
  51  		for _, cookie := range codec.cookies.Cookies(codec.url) {
  52  			httpRequest.AddCookie(cookie)
  53  		}
  54  	}
  55  
  56  	var httpResponse *http.Response
  57  	httpResponse, err = codec.httpClient.Do(httpRequest)
  58  
  59  	if err != nil {
  60  		return err
  61  	}
  62  
  63  	if codec.cookies != nil {
  64  		codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
  65  	}
  66  
  67  	codec.mutex.Lock()
  68  	codec.responses[request.Seq] = httpResponse
  69  	codec.mutex.Unlock()
  70  
  71  	codec.ready <- request.Seq
  72  
  73  	return nil
  74  }
  75  
  76  func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
  77  	var seq uint64
  78  	select {
  79  	case seq = <-codec.ready:
  80  	case <-codec.close:
  81  		return errors.New("codec is closed")
  82  	}
  83  	response.Seq = seq
  84  
  85  	codec.mutex.Lock()
  86  	httpResponse := codec.responses[seq]
  87  	delete(codec.responses, seq)
  88  	codec.mutex.Unlock()
  89  
  90  	defer httpResponse.Body.Close()
  91  
  92  	if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
  93  		response.Error = fmt.Sprintf("request error: bad status code - %d", httpResponse.StatusCode)
  94  		return nil
  95  	}
  96  
  97  	body, err := ioutil.ReadAll(httpResponse.Body)
  98  	if err != nil {
  99  		response.Error = err.Error()
 100  		return nil
 101  	}
 102  
 103  	resp := Response(body)
 104  	if err := resp.Err(); err != nil {
 105  		response.Error = err.Error()
 106  		return nil
 107  	}
 108  
 109  	codec.response = resp
 110  
 111  	return nil
 112  }
 113  
 114  func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
 115  	if v == nil {
 116  		return nil
 117  	}
 118  	return codec.response.Unmarshal(v)
 119  }
 120  
 121  func (codec *clientCodec) Close() error {
 122  	if transport, ok := codec.httpClient.Transport.(*http.Transport); ok {
 123  		transport.CloseIdleConnections()
 124  	}
 125  
 126  	close(codec.close)
 127  
 128  	return nil
 129  }
 130  
 131  // NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
 132  func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
 133  	if transport == nil {
 134  		transport = http.DefaultTransport
 135  	}
 136  
 137  	httpClient := &http.Client{Transport: transport}
 138  
 139  	jar, err := cookiejar.New(nil)
 140  
 141  	if err != nil {
 142  		return nil, err
 143  	}
 144  
 145  	u, err := url.Parse(requrl)
 146  
 147  	if err != nil {
 148  		return nil, err
 149  	}
 150  
 151  	codec := clientCodec{
 152  		url:        u,
 153  		httpClient: httpClient,
 154  		close:      make(chan uint64),
 155  		ready:      make(chan uint64),
 156  		responses:  make(map[uint64]*http.Response),
 157  		cookies:    jar,
 158  	}
 159  
 160  	return &Client{rpc.NewClientWithCodec(&codec)}, nil
 161  }
 162