1package xmlrpc
2
3import (
4	"errors"
5	"fmt"
6	"io/ioutil"
7	"net/http"
8	"net/http/cookiejar"
9	"net/rpc"
10	"net/url"
11	"sync"
12)
13
14type Client struct {
15	*rpc.Client
16}
17
18// clientCodec is rpc.ClientCodec interface implementation.
19type 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
43func (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
76func (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
114func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
115	if v == nil {
116		return nil
117	}
118	return codec.response.Unmarshal(v)
119}
120
121func (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.
132func 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