├── go.mod
├── .travis.yml
├── _example
└── example.go
├── README.md
├── LICENSE
├── xmlrpc_test.go
└── xmlrpc.go
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mattn/go-xmlrpc
2 |
3 | go 1.10
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - tip
4 | before_install:
5 | - go test
6 |
--------------------------------------------------------------------------------
/_example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/mattn/go-xmlrpc"
5 | "fmt"
6 | "log"
7 | )
8 |
9 | func main() {
10 | res, e := xmlrpc.Call(
11 | "http://your-blog.example.com/xmlrpc.php",
12 | "metaWeblog.getRecentPosts",
13 | "blog-id",
14 | "user-id",
15 | "password",
16 | 10)
17 | if e != nil {
18 | log.Fatal(e)
19 | }
20 | for _, p := range res.(xmlrpc.Array) {
21 | for k, v := range p.(xmlrpc.Struct) {
22 | fmt.Printf("%s=%v\n", k, v)
23 | }
24 | fmt.Println()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-xmlrpc
2 |
3 | xmlrpc interface for go
4 |
5 | ## Usage
6 |
7 | ```go
8 | package main
9 |
10 | import (
11 | "github.com/mattn/go-xmlrpc"
12 | "fmt"
13 | "log"
14 | )
15 |
16 | func main() {
17 | res, e := xmlrpc.Call(
18 | "http://your-blog.example.com/xmlrpc.php",
19 | "metaWeblog.getRecentPosts",
20 | "blog-id",
21 | "user-id",
22 | "password",
23 | 10)
24 | if e != nil {
25 | log.Fatal(e)
26 | }
27 | for _, p := range res.(xmlrpc.Array) {
28 | for k, v := range p.(xmlrpc.Struct) {
29 | fmt.Printf("%s=%v\n", k, v)
30 | }
31 | fmt.Println()
32 | }
33 | }
34 | ```
35 |
36 | ## Installation
37 |
38 | ```
39 | $ go get github.com/mattn/go-xmlrpc
40 | ```
41 |
42 | ## License
43 |
44 | MIT
45 |
46 | ## Author
47 |
48 | Yasuhiro Matsumoto (a.k.a. mattn)
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Yasuhiro Matsumoto
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/xmlrpc_test.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "net/http/httptest"
9 | "testing"
10 | )
11 |
12 | func createServer(path, name string, f func(args ...interface{}) (interface{}, error)) http.HandlerFunc {
13 | return func(w http.ResponseWriter, r *http.Request) {
14 | if r.URL.Path != path {
15 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
16 | return
17 | }
18 | p := xml.NewDecoder(r.Body)
19 | se, _ := nextStart(p) // methodResponse
20 | if se.Name.Local != "methodCall" {
21 | http.Error(w, "missing methodCall", http.StatusBadRequest)
22 | return
23 | }
24 | se, _ = nextStart(p) // params
25 | if se.Name.Local != "methodName" {
26 | http.Error(w, "missing methodName", http.StatusBadRequest)
27 | return
28 | }
29 | var s string
30 | if err := p.DecodeElement(&s, &se); err != nil {
31 | http.Error(w, "wrong function name", http.StatusBadRequest)
32 | return
33 | }
34 | if s != name {
35 | http.Error(w, fmt.Sprintf("want function name %q but got %q", name, s), http.StatusBadRequest)
36 | return
37 | }
38 | se, _ = nextStart(p) // params
39 | if se.Name.Local != "params" {
40 | http.Error(w, "missing params", http.StatusBadRequest)
41 | return
42 | }
43 | var args []interface{}
44 | for {
45 | se, _ = nextStart(p) // param
46 | if se.Name.Local == "" {
47 | break
48 | }
49 | if se.Name.Local != "param" {
50 | http.Error(w, "missing param", http.StatusBadRequest)
51 | return
52 | }
53 | se, _ = nextStart(p) // value
54 | if se.Name.Local != "value" {
55 | http.Error(w, "missing value", http.StatusBadRequest)
56 | return
57 | }
58 | _, v, err := next(p)
59 | if err != nil {
60 | http.Error(w, err.Error(), http.StatusBadRequest)
61 | return
62 | }
63 | args = append(args, v)
64 | }
65 |
66 | ret, err := f(args...)
67 | if err != nil {
68 | http.Error(w, err.Error(), http.StatusBadRequest)
69 | return
70 | }
71 | w.Write([]byte(`
72 |
73 |
74 |
75 |
76 | ` + toXml(ret, true) + `
77 |
78 |
79 |
80 | `))
81 | }
82 | }
83 |
84 | func TestAddInt(t *testing.T) {
85 | ts := httptest.NewServer(createServer("/api", "AddInt", func(args ...interface{}) (interface{}, error) {
86 | if len(args) != 2 {
87 | return nil, errors.New("bad number of arguments")
88 | }
89 | switch args[0].(type) {
90 | case int:
91 | default:
92 | return nil, errors.New("args[0] should be int")
93 | }
94 | switch args[1].(type) {
95 | case int:
96 | default:
97 | return nil, errors.New("args[1] should be int")
98 | }
99 | return args[0].(int) + args[1].(int), nil
100 | }))
101 | defer ts.Close()
102 |
103 | client := NewClient(ts.URL + "/api")
104 | v, err := client.Call("AddInt", 1, 2)
105 | if err != nil {
106 | t.Fatal(err)
107 | }
108 | i, ok := v.(int)
109 | if !ok {
110 | t.Fatalf("want int but got %T: %v", v, v)
111 | }
112 | if i != 3 {
113 | t.Fatalf("want %v but got %v", 3, v)
114 | }
115 | }
116 |
117 | func TestAddString(t *testing.T) {
118 | ts := httptest.NewServer(createServer("/api", "AddString", func(args ...interface{}) (interface{}, error) {
119 | if len(args) != 2 {
120 | return nil, errors.New("bad number of arguments")
121 | }
122 | switch args[0].(type) {
123 | case string:
124 | default:
125 | return nil, errors.New("args[0] should be string")
126 | }
127 | switch args[1].(type) {
128 | case string:
129 | default:
130 | return nil, errors.New("args[1] should be string")
131 | }
132 | return args[0].(string) + args[1].(string), nil
133 | }))
134 | defer ts.Close()
135 |
136 | client := NewClient(ts.URL + "/api")
137 | v, err := client.Call("AddString", "hello", "world")
138 | if err != nil {
139 | t.Fatal(err)
140 | }
141 | s, ok := v.(string)
142 | if !ok {
143 | t.Fatalf("want string but got %T: %v", v, v)
144 | }
145 | if s != "helloworld" {
146 | t.Fatalf("want %q but got %q", "helloworld", v)
147 | }
148 | }
149 |
150 | type ParseStructArrayHandler struct {
151 | }
152 |
153 | func (h *ParseStructArrayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
154 | w.Write([]byte(`
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | test1
166 |
167 | a
168 |
169 |
170 |
171 | test2
172 |
173 | 2
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | test1
182 |
183 | b
184 |
185 |
186 |
187 | test2
188 |
189 | 2
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | test1
198 |
199 | c
200 |
201 |
202 |
203 | test2
204 |
205 | 2
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | `))
217 | }
218 |
219 | func TestParseStructArray(t *testing.T) {
220 | ts := httptest.NewServer(&ParseStructArrayHandler{})
221 | defer ts.Close()
222 |
223 | res, err := NewClient(ts.URL + "/").Call("Irrelevant")
224 |
225 | if err != nil {
226 | t.Fatal(err)
227 | }
228 |
229 | if len(res.(Array)) != 3 {
230 | t.Fatal("expected array with 3 entries")
231 | }
232 | }
233 |
234 | type ParseIntArrayHandler struct {
235 | }
236 |
237 | func (h *ParseIntArrayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
238 | w.Write([]byte(`
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | 2
248 |
249 |
250 | 3
251 |
252 |
253 | 4
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | `))
262 | }
263 |
264 | func TestParseIntArray(t *testing.T) {
265 | ts := httptest.NewServer(&ParseIntArrayHandler{})
266 | defer ts.Close()
267 |
268 | res, err := NewClient(ts.URL + "/").Call("Irrelevant")
269 |
270 | if err != nil {
271 | t.Fatal(err)
272 | }
273 |
274 | if len(res.(Array)) != 3 {
275 | t.Fatal("expected array with 3 entries")
276 | }
277 | }
278 |
279 | type ParseMixedArrayHandler struct {
280 | }
281 |
282 | func (h *ParseMixedArrayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
283 | w.Write([]byte(`
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 | test1
295 |
296 | a
297 |
298 |
299 |
300 | test2
301 |
302 | 2
303 |
304 |
305 |
306 |
307 |
308 | 2
309 |
310 |
311 |
312 |
313 | test1
314 |
315 | b
316 |
317 |
318 |
319 | test2
320 |
321 | 2
322 |
323 |
324 |
325 |
326 |
327 | 4
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 | `))
336 | }
337 |
338 | func TestParseMixedArray(t *testing.T) {
339 | ts := httptest.NewServer(&ParseMixedArrayHandler{})
340 | defer ts.Close()
341 |
342 | res, err := NewClient(ts.URL + "/").Call("Irrelevant")
343 |
344 | if err != nil {
345 | t.Fatal(err)
346 | }
347 |
348 | if len(res.(Array)) != 4 {
349 | t.Fatal("expected array with 4 entries")
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/xmlrpc.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/xml"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "io/ioutil"
11 | "net/http"
12 | "reflect"
13 | "strconv"
14 | "strings"
15 | "time"
16 | )
17 |
18 | type Array []interface{}
19 | type Struct map[string]interface{}
20 |
21 | var xmlSpecial = map[byte]string{
22 | '<': "<",
23 | '>': ">",
24 | '"': """,
25 | '\'': "'",
26 | '&': "&",
27 | }
28 |
29 | func xmlEscape(s string) string {
30 | var b bytes.Buffer
31 | for i := 0; i < len(s); i++ {
32 | c := s[i]
33 | if s, ok := xmlSpecial[c]; ok {
34 | b.WriteString(s)
35 | } else {
36 | b.WriteByte(c)
37 | }
38 | }
39 | return b.String()
40 | }
41 |
42 | type valueNode struct {
43 | Type string `xml:"attr"`
44 | Body string `xml:"chardata"`
45 | }
46 |
47 | func next(p *xml.Decoder) (xml.Name, interface{}, error) {
48 | se, e := nextStart(p)
49 | if e != nil {
50 | return xml.Name{}, nil, e
51 | }
52 |
53 | var nv interface{}
54 | switch se.Name.Local {
55 | case "string":
56 | var s string
57 | if e = p.DecodeElement(&s, &se); e != nil {
58 | return xml.Name{}, nil, e
59 | }
60 | return xml.Name{}, s, nil
61 | case "boolean":
62 | var s string
63 | if e = p.DecodeElement(&s, &se); e != nil {
64 | return xml.Name{}, nil, e
65 | }
66 | s = strings.TrimSpace(s)
67 | var b bool
68 | switch s {
69 | case "true", "1":
70 | b = true
71 | case "false", "0":
72 | b = false
73 | default:
74 | e = errors.New("invalid boolean value")
75 | }
76 | return xml.Name{}, b, e
77 | case "int", "i1", "i2", "i4", "i8":
78 | var s string
79 | var i int
80 | if e = p.DecodeElement(&s, &se); e != nil {
81 | return xml.Name{}, nil, e
82 | }
83 | i, e = strconv.Atoi(strings.TrimSpace(s))
84 | return xml.Name{}, i, e
85 | case "double":
86 | var s string
87 | var f float64
88 | if e = p.DecodeElement(&s, &se); e != nil {
89 | return xml.Name{}, nil, e
90 | }
91 | f, e = strconv.ParseFloat(strings.TrimSpace(s), 64)
92 | return xml.Name{}, f, e
93 | case "dateTime.iso8601":
94 | var s string
95 | if e = p.DecodeElement(&s, &se); e != nil {
96 | return xml.Name{}, nil, e
97 | }
98 | t, e := time.Parse("20060102T15:04:05", s)
99 | if e != nil {
100 | t, e = time.Parse("2006-01-02T15:04:05-07:00", s)
101 | if e != nil {
102 | t, e = time.Parse("2006-01-02T15:04:05", s)
103 | }
104 | }
105 | return xml.Name{}, t, e
106 | case "base64":
107 | var s string
108 | if e = p.DecodeElement(&s, &se); e != nil {
109 | return xml.Name{}, nil, e
110 | }
111 | if b, e := base64.StdEncoding.DecodeString(s); e != nil {
112 | return xml.Name{}, nil, e
113 | } else {
114 | return xml.Name{}, b, nil
115 | }
116 | case "member":
117 | nextStart(p)
118 | return next(p)
119 | case "value":
120 | nextStart(p)
121 | return next(p)
122 | case "name":
123 | nextStart(p)
124 | return next(p)
125 | case "struct":
126 | st := Struct{}
127 |
128 | se, e = nextStart(p)
129 | for e == nil && se.Name.Local == "member" {
130 | // name
131 | se, e = nextStart(p)
132 | if se.Name.Local != "name" {
133 | return xml.Name{}, nil, errors.New("invalid response")
134 | }
135 | if e != nil {
136 | break
137 | }
138 | var name string
139 | if e = p.DecodeElement(&name, &se); e != nil {
140 | return xml.Name{}, nil, e
141 | }
142 | se, e = nextStart(p)
143 | if e != nil {
144 | break
145 | }
146 |
147 | // value
148 | _, value, e := next(p)
149 | if se.Name.Local != "value" {
150 | return xml.Name{}, nil, errors.New("invalid response")
151 | }
152 | if e != nil {
153 | break
154 | }
155 | st[name] = value
156 |
157 | se, e = nextStart(p)
158 | if e != nil {
159 | break
160 | }
161 | }
162 | return xml.Name{}, st, nil
163 | case "array":
164 | var ar Array
165 | nextStart(p) // data
166 | nextStart(p) // top of value
167 | for {
168 | _, value, e := next(p)
169 | if e != nil {
170 | break
171 | }
172 | ar = append(ar, value)
173 |
174 | if reflect.ValueOf(value).Kind() != reflect.Map {
175 | nextStart(p)
176 | }
177 | }
178 | return xml.Name{}, ar, nil
179 | case "nil":
180 | return xml.Name{}, nil, nil
181 | }
182 |
183 | if e = p.DecodeElement(nv, &se); e != nil {
184 | return xml.Name{}, nil, e
185 | }
186 | return se.Name, nv, e
187 | }
188 | func nextStart(p *xml.Decoder) (xml.StartElement, error) {
189 | for {
190 | t, e := p.Token()
191 | if e != nil {
192 | return xml.StartElement{}, e
193 | }
194 | switch t := t.(type) {
195 | case xml.StartElement:
196 | return t, nil
197 | }
198 | }
199 | panic("unreachable")
200 | }
201 |
202 | func toXml(v interface{}, typ bool) (s string) {
203 | if v == nil {
204 | return ""
205 | }
206 | r := reflect.ValueOf(v)
207 | t := r.Type()
208 | k := t.Kind()
209 |
210 | if b, ok := v.([]byte); ok {
211 | return "" + base64.StdEncoding.EncodeToString(b) + ""
212 | }
213 |
214 | switch k {
215 | case reflect.Invalid:
216 | panic("unsupported type")
217 | case reflect.Bool:
218 | return fmt.Sprintf("%v", v)
219 | case reflect.Int,
220 | reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
221 | reflect.Uint,
222 | reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
223 | if typ {
224 | return fmt.Sprintf("%v", v)
225 | }
226 | return fmt.Sprintf("%v", v)
227 | case reflect.Uintptr:
228 | panic("unsupported type")
229 | case reflect.Float32, reflect.Float64:
230 | if typ {
231 | return fmt.Sprintf("%v", v)
232 | }
233 | return fmt.Sprintf("%v", v)
234 | case reflect.Complex64, reflect.Complex128:
235 | panic("unsupported type")
236 | case reflect.Array:
237 | s = ""
238 | for n := 0; n < r.Len(); n++ {
239 | s += ""
240 | s += toXml(r.Index(n).Interface(), typ)
241 | s += ""
242 | }
243 | s += ""
244 | return s
245 | case reflect.Chan:
246 | panic("unsupported type")
247 | case reflect.Func:
248 | panic("unsupported type")
249 | case reflect.Interface:
250 | return toXml(r.Elem(), typ)
251 | case reflect.Map:
252 | s = ""
253 | for _, key := range r.MapKeys() {
254 | s += ""
255 | s += "" + xmlEscape(key.Interface().(string)) + ""
256 | s += "" + toXml(r.MapIndex(key).Interface(), typ) + ""
257 | s += ""
258 | }
259 | s += ""
260 | return s
261 | case reflect.Ptr:
262 | panic("unsupported type")
263 | case reflect.Slice:
264 | s = ""
265 | for n := 0; n < r.Len(); n++ {
266 | s += ""
267 | s += toXml(r.Index(n).Interface(), typ)
268 | s += ""
269 | }
270 | s += ""
271 | return s
272 | case reflect.String:
273 | if typ {
274 | return fmt.Sprintf("%v", xmlEscape(v.(string)))
275 | }
276 | return xmlEscape(v.(string))
277 | case reflect.Struct:
278 | s = ""
279 | for n := 0; n < r.NumField(); n++ {
280 | s += ""
281 | s += "" + t.Field(n).Name + ""
282 | s += "" + toXml(r.FieldByIndex([]int{n}).Interface(), true) + ""
283 | s += ""
284 | }
285 | s += ""
286 | return s
287 | case reflect.UnsafePointer:
288 | return toXml(r.Elem(), typ)
289 | }
290 | return
291 | }
292 |
293 | // Client is client of XMLRPC
294 | type Client struct {
295 | HttpClient *http.Client
296 | url string
297 | }
298 |
299 | // NewClient create new Client
300 | func NewClient(url string) *Client {
301 | return &Client{
302 | HttpClient: &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second},
303 | url: url,
304 | }
305 | }
306 |
307 | func makeRequest(name string, args ...interface{}) *bytes.Buffer {
308 | buf := new(bytes.Buffer)
309 | buf.WriteString(``)
310 | buf.WriteString("" + xmlEscape(name) + "")
311 | buf.WriteString("")
312 | for _, arg := range args {
313 | buf.WriteString("")
314 | buf.WriteString(toXml(arg, true))
315 | buf.WriteString("")
316 | }
317 | buf.WriteString("")
318 | return buf
319 | }
320 |
321 | func call(client *http.Client, url, name string, args ...interface{}) (v interface{}, e error) {
322 | r, e := client.Post(url, "text/xml", makeRequest(name, args...))
323 | if e != nil {
324 | return nil, e
325 | }
326 |
327 | // Since we do not always read the entire body, discard the rest, which
328 | // allows the http transport to reuse the connection.
329 | defer io.Copy(ioutil.Discard, r.Body)
330 | defer r.Body.Close()
331 |
332 | if r.StatusCode/100 != 2 {
333 | return nil, errors.New(http.StatusText(http.StatusBadRequest))
334 | }
335 |
336 | p := xml.NewDecoder(r.Body)
337 | se, e := nextStart(p) // methodResponse
338 | if se.Name.Local != "methodResponse" {
339 | return nil, errors.New("invalid response: missing methodResponse")
340 | }
341 | se, e = nextStart(p) // params
342 | if se.Name.Local != "params" {
343 | return nil, errors.New("invalid response: missing params")
344 | }
345 | se, e = nextStart(p) // param
346 | if se.Name.Local != "param" {
347 | return nil, errors.New("invalid response: missing param")
348 | }
349 | se, e = nextStart(p) // value
350 | if se.Name.Local != "value" {
351 | return nil, errors.New("invalid response: missing value")
352 | }
353 | _, v, e = next(p)
354 | return v, e
355 | }
356 |
357 | // Call call remote procedures function name with args
358 | func (c *Client) Call(name string, args ...interface{}) (v interface{}, e error) {
359 | return call(c.HttpClient, c.url, name, args...)
360 | }
361 |
362 | // Global httpClient allows us to pool/reuse connections and not wastefully
363 | // re-create transports for each request.
364 | var httpClient = &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second}
365 |
366 | // Call call remote procedures function name with args
367 | func Call(url, name string, args ...interface{}) (v interface{}, e error) {
368 | return call(httpClient, url, name, args...)
369 | }
370 |
--------------------------------------------------------------------------------