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