├── 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 | --------------------------------------------------------------------------------