├── .gitignore ├── LICENSE ├── README.md ├── examples ├── README.md ├── example_client.go └── example_server.go └── xml ├── client.go ├── doc.go ├── fault.go ├── fault_test.go ├── rpc2xml.go ├── rpc2xml_test.go ├── server.go ├── xml2rpc.go ├── xml2rpc_test.go └── xml_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Ivan Daniluk 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gorilla-xmlrpc # 2 | 3 | [![GoDoc](https://godoc.org/github.com/divan/gorilla-xmlrpc/xml?status.svg)](https://godoc.org/github.com/divan/gorilla-xmlrpc/xml) 4 | 5 | XML-RPC implementation for the Gorilla/RPC toolkit. 6 | 7 | It implements both server and client. 8 | 9 | It's built on top of gorilla/rpc package in Go(Golang) language and implements XML-RPC, according to [it's specification](http://xmlrpc.scripting.com/spec.html). 10 | Unlike net/rpc from Go strlib, gorilla/rpc allows usage of HTTP POST requests for RPC. 11 | 12 | ### Installation ### 13 | Assuming you already imported gorilla/rpc, use the following command: 14 | 15 | go get github.com/divan/gorilla-xmlrpc/xml 16 | 17 | ### Examples ### 18 | 19 | #### Server Example #### 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "log" 26 | "net/http" 27 | "github.com/gorilla/rpc" 28 | "github.com/divan/gorilla-xmlrpc/xml" 29 | ) 30 | 31 | type HelloService struct{} 32 | 33 | func (h *HelloService) Say(r *http.Request, args *struct{Who string}, reply *struct{Message string}) error { 34 | log.Println("Say", args.Who) 35 | reply.Message = "Hello, " + args.Who + "!" 36 | return nil 37 | } 38 | 39 | func main() { 40 | RPC := rpc.NewServer() 41 | xmlrpcCodec := xml.NewCodec() 42 | RPC.RegisterCodec(xmlrpcCodec, "text/xml") 43 | RPC.RegisterService(new(HelloService), "") 44 | http.Handle("/RPC2", RPC) 45 | 46 | log.Println("Starting XML-RPC server on localhost:1234/RPC2") 47 | log.Fatal(http.ListenAndServe(":1234", nil)) 48 | } 49 | ``` 50 | 51 | It's pretty self-explanatory and can be tested with any xmlrpc client, even raw curl request: 52 | 53 | ```bash 54 | curl -v -X POST -H "Content-Type: text/xml" -d 'HelloService.SayUser 1' http://localhost:1234/RPC2 55 | ``` 56 | 57 | #### Client Example #### 58 | 59 | Implementing client is beyond the scope of this package, but with encoding/decoding handlers it should be pretty trivial. Here is an example which works with the server introduced above. 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "log" 66 | "bytes" 67 | "net/http" 68 | "github.com/divan/gorilla-xmlrpc/xml" 69 | ) 70 | 71 | func XmlRpcCall(method string, args struct{Who string}) (reply struct{Message string}, err error) { 72 | buf, _ := xml.EncodeClientRequest(method, &args) 73 | 74 | resp, err := http.Post("http://localhost:1234/RPC2", "text/xml", bytes.NewBuffer(buf)) 75 | if err != nil { 76 | return 77 | } 78 | defer resp.Body.Close() 79 | 80 | err = xml.DecodeClientResponse(resp.Body, &reply) 81 | return 82 | } 83 | 84 | func main() { 85 | reply, err := XmlRpcCall("HelloService.Say", struct{Who string}{"User 1"}) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | log.Printf("Response: %s\n", reply.Message) 91 | } 92 | 93 | ``` 94 | 95 | ### Implementation details ### 96 | 97 | The main objective was to use standard encoding/xml package for XML marshalling/unmarshalling. Unfortunately, in current implementation there is no graceful way to implement common structre for marshal and unmarshal functions - marshalling doesn't handle interface{} types so far (though, it could be changed in the future). 98 | So, marshalling is implemented manually. 99 | 100 | Unmarshalling code first creates temporary structure for unmarshalling XML into, then converts it into the passed variable using *reflect* package. 101 | If XML struct member's name is lowercased, it's first letter will be uppercased, as in Go/Gorilla field name must be exported(first-letter uppercased). 102 | 103 | Marshalling code converts rpc directly to the string XML representation. 104 | 105 | For the better understanding, I use terms 'rpc2xml' and 'xml2rpc' instead of 'marshal' and 'unmarshall'. 106 | 107 | ### Supported types ### 108 | 109 | | XML-RPC | Golang | 110 | | ---------------- | ------------- | 111 | | int, i4 | int | 112 | | double | float64 | 113 | | boolean | bool | 114 | | string | string | 115 | | dateTime.iso8601 | time.Time | 116 | | base64 | []byte | 117 | | struct | struct | 118 | | array | []interface{} | 119 | | nil | nil | 120 | 121 | ### TODO ### 122 | 123 | * Add more corner cases tests 124 | 125 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | You can test server and client implementations by running following code. 4 | 5 | In console: 6 | ```bash 7 | go run example_server.go 8 | ``` 9 | 10 | In another console: 11 | ```bash 12 | go run example_client.go 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/example_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "bytes" 6 | "net/http" 7 | "github.com/divan/gorilla-xmlrpc/xml" 8 | ) 9 | 10 | func XmlRpcCall(method string, args struct{Who string}) (reply struct{Message string}, err error) { 11 | buf, _ := xml.EncodeClientRequest(method, &args) 12 | 13 | resp, err := http.Post("http://localhost:1234/RPC2", "text/xml", bytes.NewBuffer(buf)) 14 | if err != nil { 15 | return 16 | } 17 | defer resp.Body.Close() 18 | 19 | err = xml.DecodeClientResponse(resp.Body, &reply) 20 | return 21 | } 22 | 23 | func main() { 24 | reply, err := XmlRpcCall("HelloService.Say", struct{Who string}{"User 1"}) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | log.Printf("Response: %s\n", reply.Message) 30 | } -------------------------------------------------------------------------------- /examples/example_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "github.com/gorilla/rpc" 7 | "github.com/divan/gorilla-xmlrpc/xml" 8 | ) 9 | 10 | type HelloService struct{} 11 | 12 | func (h *HelloService) Say(r *http.Request, args *struct{Who string}, reply *struct{Message string}) error { 13 | log.Println("Say", args.Who) 14 | reply.Message = "Hello, " + args.Who + "!" 15 | return nil 16 | } 17 | 18 | func main() { 19 | RPC := rpc.NewServer() 20 | xmlrpcCodec := xml.NewCodec() 21 | RPC.RegisterCodec(xmlrpcCodec, "text/xml") 22 | RPC.RegisterService(new(HelloService), "") 23 | http.Handle("/RPC2", RPC) 24 | 25 | log.Println("Starting XML-RPC server on localhost:1234/RPC2") 26 | log.Fatal(http.ListenAndServe(":1234", nil)) 27 | } -------------------------------------------------------------------------------- /xml/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "io" 9 | "io/ioutil" 10 | ) 11 | 12 | // EncodeClientRequest encodes parameters for a XML-RPC client request. 13 | func EncodeClientRequest(method string, args interface{}) ([]byte, error) { 14 | xml, err := rpcRequest2XML(method, args) 15 | return []byte(xml), err 16 | } 17 | 18 | // DecodeClientResponse decodes the response body of a client request into 19 | // the interface reply. 20 | func DecodeClientResponse(r io.Reader, reply interface{}) error { 21 | rawxml, err := ioutil.ReadAll(r) 22 | if err != nil { 23 | return FaultSystemError 24 | } 25 | return xml2RPC(string(rawxml), reply) 26 | } 27 | -------------------------------------------------------------------------------- /xml/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | XML-RPC implementation for the Gorilla/RPC toolkit. 3 | 4 | It's built on top of gorilla/rpc package in Go(Golang) language and implements XML-RPC, according to it's specification. Unlike net/rpc from Go strlib, gorilla/rpc allows usage of HTTP POST requests for RPC. 5 | 6 | XML-RPC spec: http://xmlrpc.scripting.com/spec.html 7 | 8 | Installation 9 | 10 | Assuming you already imported gorilla/rpc, use the following command: 11 | 12 | go get github.com/divan/gorilla-xmlrpc/xml 13 | 14 | Implementation details 15 | 16 | The main objective was to use standard encoding/xml package for XML marshalling/unmarshalling. Unfortunately, in current implementation there is no graceful way to implement common structre for marshal and unmarshal functions - marshalling doesn't handle interface{} types so far (though, it could be changed in the future). So, marshalling is implemented manually. 17 | 18 | Unmarshalling code first creates temporary structure for unmarshalling XML into, then converts it into the passed variable using reflect package. If XML struct member's name is lowercased, it's first letter will be uppercased, as in Go/Gorilla field name must be exported(first-letter uppercased). 19 | 20 | Marshalling code converts rpc directly to the string XML representation. 21 | 22 | For the better understanding, I use terms 'rpc2xml' and 'xml2rpc' instead of 'marshal' and 'unmarshall'. 23 | 24 | Types 25 | 26 | The following types are supported: 27 | 28 | XML-RPC Golang 29 | ------- ------ 30 | int, i4 int 31 | double float64 32 | boolean bool 33 | stringi string 34 | dateTime.iso8601 time.Time 35 | base64 []byte 36 | struct struct 37 | array []interface{} 38 | nil nil 39 | 40 | TODO 41 | 42 | TODO list: 43 | * Add more corner cases tests 44 | 45 | Examples 46 | 47 | Checkout examples in examples/ directory. 48 | 49 | */ 50 | package xml 51 | -------------------------------------------------------------------------------- /xml/fault.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | // Default Faults 12 | // NOTE: XMLRPC spec doesn't specify any Fault codes. 13 | // These codes seems to be widely accepted, and taken from the http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php 14 | var ( 15 | FaultInvalidParams = Fault{Code: -32602, String: "Invalid Method Parameters"} 16 | FaultWrongArgumentsNumber = Fault{Code: -32602, String: "Wrong Arguments Number"} 17 | FaultInternalError = Fault{Code: -32603, String: "Internal Server Error"} 18 | FaultApplicationError = Fault{Code: -32500, String: "Application Error"} 19 | FaultSystemError = Fault{Code: -32400, String: "System Error"} 20 | FaultDecode = Fault{Code: -32700, String: "Parsing error: not well formed"} 21 | ) 22 | 23 | // Fault represents XML-RPC Fault. 24 | type Fault struct { 25 | Code int `xml:"faultCode"` 26 | String string `xml:"faultString"` 27 | } 28 | 29 | // Error satisifies error interface for Fault. 30 | func (f Fault) Error() string { 31 | return fmt.Sprintf("%d: %s", f.Code, f.String) 32 | } 33 | 34 | // Fault2XML is a quick 'marshalling' replacemnt for the Fault case. 35 | func fault2XML(fault Fault) string { 36 | buffer := "" 37 | xml, _ := rpc2XML(fault) 38 | buffer += xml 39 | buffer += "" 40 | return buffer 41 | } 42 | 43 | type faultValue struct { 44 | Value value `xml:"value"` 45 | } 46 | 47 | // IsEmpty returns true if faultValue contain fault. 48 | // 49 | // faultValue should be a struct with 2 members. 50 | func (f faultValue) IsEmpty() bool { 51 | return len(f.Value.Struct) == 0 52 | } 53 | -------------------------------------------------------------------------------- /xml/fault_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/gorilla/rpc" 13 | ) 14 | 15 | ////////////////////////////////// 16 | // Service 1 17 | ////////////////////////////////// 18 | type FaultTestRequest struct { 19 | A int 20 | B int 21 | } 22 | 23 | type FaultTestBadRequest struct { 24 | A int 25 | B int 26 | C int 27 | } 28 | 29 | type FaultTestResponse struct { 30 | Result int 31 | } 32 | 33 | type FaultTestBadResponse struct { 34 | Result string 35 | } 36 | 37 | type FaultTest struct { 38 | } 39 | 40 | func (t *FaultTest) Multiply(r *http.Request, req *FaultTestRequest, res *FaultTestResponse) error { 41 | res.Result = req.A * req.B 42 | return nil 43 | } 44 | 45 | func TestFaults(t *testing.T) { 46 | s := rpc.NewServer() 47 | s.RegisterCodec(NewCodec(), "text/xml") 48 | s.RegisterService(new(FaultTest), "") 49 | 50 | var err error 51 | 52 | var res1 FaultTestResponse 53 | err = execute(t, s, "FaultTest.Multiply", &FaultTestBadRequest{4, 2, 4}, &res1) 54 | if err == nil { 55 | t.Fatal("expected err to be not nil, but got:", err) 56 | } 57 | fault, ok := err.(Fault) 58 | if !ok { 59 | t.Fatal("expected error to be of concrete type Fault, but got", err) 60 | } 61 | if fault.Code != -32602 { 62 | t.Errorf("wrong fault code: %d", fault.Code) 63 | } 64 | if fault.String != "Wrong Arguments Number" { 65 | t.Errorf("wrong fault string: %s", fault.String) 66 | } 67 | 68 | var res2 FaultTestBadResponse 69 | err = execute(t, s, "FaultTest.Multiply", &FaultTestRequest{4, 2}, &res2) 70 | if err == nil { 71 | t.Fatal("expected err to be not nil, but got:", err) 72 | } 73 | fault, ok = err.(Fault) 74 | if !ok { 75 | t.Fatal("expected error to be of concrete type Fault, but got", err) 76 | } 77 | if fault.Code != -32602 { 78 | t.Errorf("wrong fault code: %d", fault.Code) 79 | } 80 | 81 | if !strings.HasPrefix(fault.String, "Invalid Method Parameters: fields type mismatch") { 82 | t.Errorf("wrong response: %s", fault.String) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /xml/rpc2xml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "encoding/base64" 9 | "fmt" 10 | "reflect" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | func rpcRequest2XML(method string, rpc interface{}) (string, error) { 16 | buffer := "" 17 | buffer += method 18 | buffer += "" 19 | params, err := rpcParams2XML(rpc) 20 | buffer += params 21 | buffer += "" 22 | return buffer, err 23 | } 24 | 25 | func rpcResponse2XML(rpc interface{}) (string, error) { 26 | buffer := "" 27 | params, err := rpcParams2XML(rpc) 28 | buffer += params 29 | buffer += "" 30 | return buffer, err 31 | } 32 | 33 | func rpcParams2XML(rpc interface{}) (string, error) { 34 | var err error 35 | buffer := "" 36 | for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { 37 | var xml string 38 | buffer += "" 39 | xml, err = rpc2XML(reflect.ValueOf(rpc).Elem().Field(i).Interface()) 40 | buffer += xml 41 | buffer += "" 42 | } 43 | buffer += "" 44 | return buffer, err 45 | } 46 | 47 | func rpc2XML(value interface{}) (string, error) { 48 | out := "" 49 | switch reflect.ValueOf(value).Kind() { 50 | case reflect.Int: 51 | out += fmt.Sprintf("%d", value.(int)) 52 | case reflect.Float64: 53 | out += fmt.Sprintf("%f", value.(float64)) 54 | case reflect.String: 55 | out += string2XML(value.(string)) 56 | case reflect.Bool: 57 | out += bool2XML(value.(bool)) 58 | case reflect.Struct: 59 | if reflect.TypeOf(value).String() != "time.Time" { 60 | out += struct2XML(value) 61 | } else { 62 | out += time2XML(value.(time.Time)) 63 | } 64 | case reflect.Slice, reflect.Array: 65 | // FIXME: is it the best way to recognize '[]byte'? 66 | if reflect.TypeOf(value).String() != "[]uint8" { 67 | out += array2XML(value) 68 | } else { 69 | out += base642XML(value.([]byte)) 70 | } 71 | case reflect.Ptr: 72 | if reflect.ValueOf(value).IsNil() { 73 | out += "" 74 | } 75 | } 76 | out += "" 77 | return out, nil 78 | } 79 | 80 | func bool2XML(value bool) string { 81 | var b string 82 | if value { 83 | b = "1" 84 | } else { 85 | b = "0" 86 | } 87 | return fmt.Sprintf("%s", b) 88 | } 89 | 90 | func string2XML(value string) string { 91 | value = strings.Replace(value, "&", "&", -1) 92 | value = strings.Replace(value, "\"", """, -1) 93 | value = strings.Replace(value, "<", "<", -1) 94 | value = strings.Replace(value, ">", ">", -1) 95 | return fmt.Sprintf("%s", value) 96 | } 97 | 98 | func struct2XML(value interface{}) (out string) { 99 | out += "" 100 | for i := 0; i < reflect.TypeOf(value).NumField(); i++ { 101 | field := reflect.ValueOf(value).Field(i) 102 | field_type := reflect.TypeOf(value).Field(i) 103 | var name string 104 | if field_type.Tag.Get("xml") != "" { 105 | name = field_type.Tag.Get("xml") 106 | } else { 107 | name = field_type.Name 108 | } 109 | field_value, _ := rpc2XML(field.Interface()) 110 | field_name := fmt.Sprintf("%s", name) 111 | out += fmt.Sprintf("%s%s", field_name, field_value) 112 | } 113 | out += "" 114 | return 115 | } 116 | 117 | func array2XML(value interface{}) (out string) { 118 | out += "" 119 | for i := 0; i < reflect.ValueOf(value).Len(); i++ { 120 | item_xml, _ := rpc2XML(reflect.ValueOf(value).Index(i).Interface()) 121 | out += item_xml 122 | } 123 | out += "" 124 | return 125 | } 126 | 127 | func time2XML(t time.Time) string { 128 | /* 129 | // TODO: find out whether we need to deal 130 | // here with TZ 131 | var tz string; 132 | zone, offset := t.Zone() 133 | if zone == "UTC" { 134 | tz = "Z" 135 | } else { 136 | tz = fmt.Sprintf("%03d00", offset / 3600 ) 137 | } 138 | */ 139 | return fmt.Sprintf("%04d%02d%02dT%02d:%02d:%02d", 140 | t.Year(), t.Month(), t.Day(), 141 | t.Hour(), t.Minute(), t.Second()) 142 | } 143 | 144 | func base642XML(data []byte) string { 145 | str := base64.StdEncoding.EncodeToString(data) 146 | return fmt.Sprintf("%s", str) 147 | } 148 | -------------------------------------------------------------------------------- /xml/rpc2xml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type SubStructRpc2Xml struct { 13 | Foo int 14 | Bar string 15 | Data []int 16 | } 17 | 18 | type StructRpc2Xml struct { 19 | Int int 20 | Float float64 21 | Str string 22 | Bool bool 23 | Sub SubStructRpc2Xml 24 | Time time.Time 25 | Base64 []byte 26 | } 27 | 28 | func TestRPC2XML(t *testing.T) { 29 | req := &StructRpc2Xml{123, 3.145926, "Hello, World!", false, SubStructRpc2Xml{42, "I'm Bar", []int{1, 2, 3}}, time.Date(2012, time.July, 17, 14, 8, 55, 0, time.Local), []byte("you can't read this!")} 30 | xml, err := rpcRequest2XML("Some.Method", req) 31 | if err != nil { 32 | t.Error("RPC2XML conversion failed", err) 33 | } 34 | expected := "Some.Method1233.145926Hello, World!0Foo42BarI'm BarData12320120717T14:08:55eW91IGNhbid0IHJlYWQgdGhpcyE=" 35 | if xml != expected { 36 | t.Error("RPC2XML conversion failed") 37 | t.Error("Expected", expected) 38 | t.Error("Got", xml) 39 | } 40 | } 41 | 42 | type StructSpecialCharsRpc2Xml struct { 43 | String1 string 44 | } 45 | 46 | func TestRPC2XMLSpecialChars(t *testing.T) { 47 | req := &StructSpecialCharsRpc2Xml{" & \" < > "} 48 | xml, err := rpcResponse2XML(req) 49 | if err != nil { 50 | t.Error("RPC2XML conversion failed", err) 51 | } 52 | expected := " & " < > " 53 | if xml != expected { 54 | t.Error("RPC2XML Special chars conversion failed") 55 | t.Error("Expected", expected) 56 | t.Error("Got", xml) 57 | } 58 | } 59 | 60 | type StructNilRpc2Xml struct { 61 | Ptr *int 62 | } 63 | 64 | func TestRpc2XmlNil(t *testing.T) { 65 | req := &StructNilRpc2Xml{nil} 66 | xml, err := rpcResponse2XML(req) 67 | if err != nil { 68 | t.Error("RPC2XML conversion failed", err) 69 | } 70 | expected := "" 71 | if xml != expected { 72 | t.Error("RPC2XML Special chars conversion failed") 73 | t.Error("Expected", expected) 74 | t.Error("Got", xml) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /xml/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "encoding/xml" 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | 13 | "github.com/gorilla/rpc" 14 | ) 15 | 16 | // ---------------------------------------------------------------------------- 17 | // Codec 18 | // ---------------------------------------------------------------------------- 19 | 20 | // NewCodec returns a new XML-RPC Codec. 21 | func NewCodec() *Codec { 22 | return &Codec{ 23 | aliases: make(map[string]string), 24 | } 25 | } 26 | 27 | // Codec creates a CodecRequest to process each request. 28 | type Codec struct { 29 | aliases map[string]string 30 | } 31 | 32 | // RegisterAlias creates a method alias 33 | func (c *Codec) RegisterAlias(alias, method string) { 34 | c.aliases[alias] = method 35 | } 36 | 37 | // NewRequest returns a CodecRequest. 38 | func (c *Codec) NewRequest(r *http.Request) rpc.CodecRequest { 39 | rawxml, err := ioutil.ReadAll(r.Body) 40 | if err != nil { 41 | return &CodecRequest{err: err} 42 | } 43 | defer r.Body.Close() 44 | 45 | var request ServerRequest 46 | if err := xml.Unmarshal(rawxml, &request); err != nil { 47 | return &CodecRequest{err: err} 48 | } 49 | request.rawxml = string(rawxml) 50 | if method, ok := c.aliases[request.Method]; ok { 51 | request.Method = method 52 | } 53 | return &CodecRequest{request: &request} 54 | } 55 | 56 | // ---------------------------------------------------------------------------- 57 | // CodecRequest 58 | // ---------------------------------------------------------------------------- 59 | 60 | type ServerRequest struct { 61 | Name xml.Name `xml:"methodCall"` 62 | Method string `xml:"methodName"` 63 | rawxml string 64 | } 65 | 66 | // CodecRequest decodes and encodes a single request. 67 | type CodecRequest struct { 68 | request *ServerRequest 69 | err error 70 | } 71 | 72 | // Method returns the RPC method for the current request. 73 | // 74 | // The method uses a dotted notation as in "Service.Method". 75 | func (c *CodecRequest) Method() (string, error) { 76 | if c.err == nil { 77 | return c.request.Method, nil 78 | } 79 | return "", c.err 80 | } 81 | 82 | // ReadRequest fills the request object for the RPC method. 83 | // 84 | // args is the pointer to the Service.Args structure 85 | // it gets populated from temporary XML structure 86 | func (c *CodecRequest) ReadRequest(args interface{}) error { 87 | c.err = xml2RPC(c.request.rawxml, args) 88 | return nil 89 | } 90 | 91 | // WriteResponse encodes the response and writes it to the ResponseWriter. 92 | // 93 | // response is the pointer to the Service.Response structure 94 | // it gets encoded into the XML-RPC xml string 95 | func (c *CodecRequest) WriteResponse(w http.ResponseWriter, response interface{}, methodErr error) error { 96 | var xmlstr string 97 | if c.err != nil { 98 | var fault Fault 99 | switch c.err.(type) { 100 | case Fault: 101 | fault = c.err.(Fault) 102 | default: 103 | fault = FaultApplicationError 104 | fault.String += fmt.Sprintf(": %v", c.err) 105 | } 106 | xmlstr = fault2XML(fault) 107 | } else { 108 | xmlstr, _ = rpcResponse2XML(response) 109 | } 110 | 111 | w.Header().Set("Content-Type", "text/xml; charset=utf-8") 112 | w.Write([]byte(xmlstr)) 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /xml/xml2rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "encoding/xml" 11 | "fmt" 12 | "reflect" 13 | "strconv" 14 | "time" 15 | "unicode" 16 | "unicode/utf8" 17 | 18 | "github.com/rogpeppe/go-charset/charset" 19 | _ "github.com/rogpeppe/go-charset/data" 20 | ) 21 | 22 | // Types used for unmarshalling 23 | type response struct { 24 | Name xml.Name `xml:"methodResponse"` 25 | Params []param `xml:"params>param"` 26 | Fault faultValue `xml:"fault,omitempty"` 27 | } 28 | 29 | type param struct { 30 | Value value `xml:"value"` 31 | } 32 | 33 | type value struct { 34 | Array []value `xml:"array>data>value"` 35 | Struct []member `xml:"struct>member"` 36 | String string `xml:"string"` 37 | Int string `xml:"int"` 38 | Int4 string `xml:"i4"` 39 | Double string `xml:"double"` 40 | Boolean string `xml:"boolean"` 41 | DateTime string `xml:"dateTime.iso8601"` 42 | Base64 string `xml:"base64"` 43 | Raw string `xml:",innerxml"` // the value can be defualt string 44 | } 45 | 46 | type member struct { 47 | Name string `xml:"name"` 48 | Value value `xml:"value"` 49 | } 50 | 51 | func xml2RPC(xmlraw string, rpc interface{}) error { 52 | // Unmarshal raw XML into the temporal structure 53 | var ret response 54 | decoder := xml.NewDecoder(bytes.NewReader([]byte(xmlraw))) 55 | decoder.CharsetReader = charset.NewReader 56 | err := decoder.Decode(&ret) 57 | if err != nil { 58 | return FaultDecode 59 | } 60 | 61 | if !ret.Fault.IsEmpty() { 62 | return getFaultResponse(ret.Fault) 63 | } 64 | 65 | // Structures should have equal number of fields 66 | if reflect.TypeOf(rpc).Elem().NumField() != len(ret.Params) { 67 | return FaultWrongArgumentsNumber 68 | } 69 | 70 | // Now, convert temporal structure into the 71 | // passed rpc variable, according to it's structure 72 | for i, param := range ret.Params { 73 | field := reflect.ValueOf(rpc).Elem().Field(i) 74 | err = value2Field(param.Value, &field) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // getFaultResponse converts faultValue to Fault. 84 | func getFaultResponse(fault faultValue) Fault { 85 | var ( 86 | code int 87 | str string 88 | ) 89 | 90 | for _, field := range fault.Value.Struct { 91 | if field.Name == "faultCode" { 92 | code, _ = strconv.Atoi(field.Value.Int) 93 | } else if field.Name == "faultString" { 94 | str = field.Value.String 95 | if str == "" { 96 | str = field.Value.Raw 97 | } 98 | } 99 | } 100 | 101 | return Fault{Code: code, String: str} 102 | } 103 | 104 | func value2Field(value value, field *reflect.Value) error { 105 | if !field.CanSet() { 106 | return FaultApplicationError 107 | } 108 | 109 | var ( 110 | err error 111 | val interface{} 112 | ) 113 | 114 | switch { 115 | case value.Int != "": 116 | val, _ = strconv.Atoi(value.Int) 117 | case value.Int4 != "": 118 | val, _ = strconv.Atoi(value.Int4) 119 | case value.Double != "": 120 | val, _ = strconv.ParseFloat(value.Double, 64) 121 | case value.String != "": 122 | val = value.String 123 | case value.Boolean != "": 124 | val = xml2Bool(value.Boolean) 125 | case value.DateTime != "": 126 | val, err = xml2DateTime(value.DateTime) 127 | case value.Base64 != "": 128 | val, err = xml2Base64(value.Base64) 129 | case len(value.Struct) != 0: 130 | if field.Kind() != reflect.Struct { 131 | fault := FaultInvalidParams 132 | fault.String += fmt.Sprintf("structure fields mismatch: %s != %s", field.Kind(), reflect.Struct.String()) 133 | return fault 134 | } 135 | s := value.Struct 136 | for i := 0; i < len(s); i++ { 137 | // Uppercase first letter for field name to deal with 138 | // methods in lowercase, which cannot be used 139 | field_name := uppercaseFirst(s[i].Name) 140 | f := field.FieldByName(field_name) 141 | err = value2Field(s[i].Value, &f) 142 | } 143 | case len(value.Array) != 0: 144 | a := value.Array 145 | f := *field 146 | slice := reflect.MakeSlice(reflect.TypeOf(f.Interface()), 147 | len(a), len(a)) 148 | for i := 0; i < len(a); i++ { 149 | item := slice.Index(i) 150 | err = value2Field(a[i], &item) 151 | } 152 | f = reflect.AppendSlice(f, slice) 153 | val = f.Interface() 154 | case len(value.Array) == 0: 155 | val = val 156 | 157 | default: 158 | // value field is default to string, see http://en.wikipedia.org/wiki/XML-RPC#Data_types 159 | // also can be 160 | if value.Raw != "" { 161 | val = value.Raw 162 | } 163 | } 164 | 165 | if val != nil { 166 | if reflect.TypeOf(val) != reflect.TypeOf(field.Interface()) { 167 | fault := FaultInvalidParams 168 | fault.String += fmt.Sprintf(": fields type mismatch: %s != %s", 169 | reflect.TypeOf(val), 170 | reflect.TypeOf(field.Interface())) 171 | return fault 172 | } 173 | 174 | field.Set(reflect.ValueOf(val)) 175 | } 176 | 177 | return err 178 | } 179 | 180 | func xml2Bool(value string) bool { 181 | var b bool 182 | switch value { 183 | case "1", "true", "TRUE", "True": 184 | b = true 185 | case "0", "false", "FALSE", "False": 186 | b = false 187 | } 188 | return b 189 | } 190 | 191 | func xml2DateTime(value string) (time.Time, error) { 192 | var ( 193 | year, month, day int 194 | hour, minute, second int 195 | ) 196 | _, err := fmt.Sscanf(value, "%04d%02d%02dT%02d:%02d:%02d", 197 | &year, &month, &day, 198 | &hour, &minute, &second) 199 | t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local) 200 | return t, err 201 | } 202 | 203 | func xml2Base64(value string) ([]byte, error) { 204 | return base64.StdEncoding.DecodeString(value) 205 | } 206 | 207 | func uppercaseFirst(in string) (out string) { 208 | r, n := utf8.DecodeRuneInString(in) 209 | return string(unicode.ToUpper(r)) + in[n:] 210 | } 211 | -------------------------------------------------------------------------------- /xml/xml2rpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type SubStructXml2Rpc struct { 14 | Foo int 15 | Bar string 16 | Data []int 17 | } 18 | 19 | type StructXml2Rpc struct { 20 | Int int 21 | Float float64 22 | Str string 23 | Bool bool 24 | Sub SubStructXml2Rpc 25 | Time time.Time 26 | Base64 []byte 27 | } 28 | 29 | func TestXML2RPC(t *testing.T) { 30 | req := new(StructXml2Rpc) 31 | err := xml2RPC("Some.Method1233.145926Hello, World!0Foo42BarI'm BarData12320120717T14:08:55eW91IGNhbid0IHJlYWQgdGhpcyE=", req) 32 | if err != nil { 33 | t.Error("XML2RPC conversion failed", err) 34 | } 35 | expected_req := &StructXml2Rpc{123, 3.145926, "Hello, World!", false, SubStructXml2Rpc{42, "I'm Bar", []int{1, 2, 3}}, time.Date(2012, time.July, 17, 14, 8, 55, 0, time.Local), []byte("you can't read this!")} 36 | if !reflect.DeepEqual(req, expected_req) { 37 | t.Error("XML2RPC conversion failed") 38 | t.Error("Expected", expected_req) 39 | t.Error("Got", req) 40 | } 41 | } 42 | 43 | type StructSpecialCharsXml2Rpc struct { 44 | String1 string 45 | } 46 | 47 | func TestXML2RPCSpecialChars(t *testing.T) { 48 | req := new(StructSpecialCharsXml2Rpc) 49 | err := xml2RPC(" & " < > ", req) 50 | if err != nil { 51 | t.Error("XML2RPC conversion failed", err) 52 | } 53 | expected_req := &StructSpecialCharsXml2Rpc{" & \" < > "} 54 | if !reflect.DeepEqual(req, expected_req) { 55 | t.Error("XML2RPC conversion failed") 56 | t.Error("Expected", expected_req) 57 | t.Error("Got", req) 58 | } 59 | } 60 | 61 | type StructNilXml2Rpc struct { 62 | Ptr *int 63 | } 64 | 65 | func TestXML2RPCNil(t *testing.T) { 66 | req := new(StructNilXml2Rpc) 67 | err := xml2RPC("", req) 68 | if err != nil { 69 | t.Error("XML2RPC conversion failed", err) 70 | } 71 | expected_req := &StructNilXml2Rpc{nil} 72 | if !reflect.DeepEqual(req, expected_req) { 73 | t.Error("XML2RPC conversion failed") 74 | t.Error("Expected", expected_req) 75 | t.Error("Got", req) 76 | } 77 | } 78 | 79 | type StructXml2RpcSubArgs struct { 80 | String1 string 81 | String2 string 82 | Id int 83 | } 84 | 85 | type StructXml2RpcHelloArgs struct { 86 | Args StructXml2RpcSubArgs 87 | } 88 | 89 | func TestXML2RPCLowercasedMethods(t *testing.T) { 90 | req := new(StructXml2RpcHelloArgs) 91 | err := xml2RPC("string1I'm a first stringstring2I'm a second stringid1", req) 92 | if err != nil { 93 | t.Error("XML2RPC conversion failed", err) 94 | } 95 | args := StructXml2RpcSubArgs{"I'm a first string", "I'm a second string", 1} 96 | expected_req := &StructXml2RpcHelloArgs{args} 97 | if !reflect.DeepEqual(req, expected_req) { 98 | t.Error("XML2RPC conversion failed") 99 | t.Error("Expected", expected_req) 100 | t.Error("Got", req) 101 | } 102 | } 103 | 104 | func TestXML2PRCFaultCall(t *testing.T) { 105 | req := new(StructXml2RpcHelloArgs) 106 | data := `faultCode116faultStringError 107 | Requiredattribute'user'notfound: 108 | [{'User',"gggg"},{'Host',"sss.com"},{'Password',"ssddfsdf"}] 109 | ` 110 | 111 | errstr := `Error 112 | Requiredattribute'user'notfound: 113 | [{'User',"gggg"},{'Host',"sss.com"},{'Password',"ssddfsdf"}] 114 | ` 115 | 116 | err := xml2RPC(data, req) 117 | 118 | fault, ok := err.(Fault) 119 | if !ok { 120 | t.Errorf("error should be of concrete type Fault, but got %v", err) 121 | } else { 122 | if fault.Code != 116 { 123 | t.Errorf("expected fault.Code to be %d, but got %d", 116, fault.Code) 124 | } 125 | if fault.String != errstr { 126 | t.Errorf("fault.String should be:\n\n%s\n\nbut got:\n\n%s\n", errstr, fault.String) 127 | } 128 | } 129 | } 130 | 131 | func TestXML2PRCISO88591(t *testing.T) { 132 | req := new(StructXml2RpcHelloArgs) 133 | data := `faultCode116faultStringError 134 | Requiredattribute'user'notfound: 135 | [{'User',"` + "\xd6\xf1\xe4" + `"},{'Host',"sss.com"},{'Password',"ssddfsdf"}] 136 | ` 137 | 138 | errstr := `Error 139 | Requiredattribute'user'notfound: 140 | [{'User',"Öñä"},{'Host',"sss.com"},{'Password',"ssddfsdf"}] 141 | ` 142 | 143 | err := xml2RPC(data, req) 144 | 145 | fault, ok := err.(Fault) 146 | if !ok { 147 | t.Errorf("error should be of concrete type Fault, but got %v", err) 148 | } else { 149 | if fault.Code != 116 { 150 | t.Errorf("expected fault.Code to be %d, but got %d", 116, fault.Code) 151 | } 152 | if fault.String != errstr { 153 | t.Errorf("fault.String should be:\n\n%s\n\nbut got:\n\n%s\n", errstr, fault.String) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /xml/xml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ivan Danyliuk 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package xml 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "strconv" 12 | "testing" 13 | 14 | "github.com/gorilla/rpc" 15 | ) 16 | 17 | ////////////////////////////////// 18 | // Service 1 19 | ////////////////////////////////// 20 | type Service1Request struct { 21 | A int 22 | B int 23 | } 24 | 25 | type Service1BadRequest struct { 26 | A int 27 | B int 28 | C int 29 | } 30 | 31 | type Service1Response struct { 32 | Result int 33 | } 34 | 35 | type Service1 struct { 36 | } 37 | 38 | func (t *Service1) Multiply(r *http.Request, req *Service1Request, res *Service1Response) error { 39 | res.Result = req.A * req.B 40 | return nil 41 | } 42 | 43 | ////////////////////////////////// 44 | // Service 2 45 | ////////////////////////////////// 46 | type Service2Request struct { 47 | Name string 48 | Age int 49 | HasPermit bool 50 | } 51 | 52 | type Service2Response struct { 53 | Message string 54 | Status int 55 | } 56 | 57 | type Service2 struct { 58 | } 59 | 60 | func (t *Service2) GetGreeting(r *http.Request, req *Service2Request, res *Service2Response) error { 61 | res.Message = "Hello, user " + req.Name + ". You're " + strconv.Itoa(req.Age) + " years old :-P" 62 | if req.HasPermit { 63 | res.Message += " And you has permit." 64 | } else { 65 | res.Message += " And you DON'T has permit." 66 | } 67 | res.Status = 42 68 | return nil 69 | } 70 | 71 | ////////////////////////////////// 72 | // Service 3 73 | ////////////////////////////////// 74 | 75 | type Address struct { 76 | Number int 77 | Street string 78 | Country string 79 | } 80 | 81 | type Person struct { 82 | Name string 83 | Surname string 84 | Age int 85 | Address Address 86 | } 87 | 88 | type Info struct { 89 | Facebook string 90 | Twitter string 91 | Phone string 92 | } 93 | 94 | type Service3Request struct { 95 | Person Person 96 | } 97 | 98 | type Service3Response struct { 99 | Info Info 100 | } 101 | 102 | type Service3 struct { 103 | } 104 | 105 | func (t *Service3) GetInfo(r *http.Request, req *Service3Request, res *Service3Response) error { 106 | var i Info 107 | i.Facebook = "http://facebook.com/" + req.Person.Name 108 | i.Twitter = "http://twitter.com/" + req.Person.Name 109 | i.Phone = "+55-555-555-55-55" 110 | res.Info = i 111 | return nil 112 | } 113 | 114 | func execute(t *testing.T, s *rpc.Server, method string, req, res interface{}) error { 115 | if !s.HasMethod(method) { 116 | t.Fatal("Expected to be registered:", method) 117 | } 118 | 119 | buf, _ := EncodeClientRequest(method, req) 120 | body := bytes.NewBuffer(buf) 121 | r, _ := http.NewRequest("POST", "http://localhost:8080/", body) 122 | r.Header.Set("Content-Type", "text/xml") 123 | 124 | w := httptest.NewRecorder() 125 | s.ServeHTTP(w, r) 126 | 127 | return DecodeClientResponse(w.Body, res) 128 | } 129 | 130 | func TestRPC2XMLConverter(t *testing.T) { 131 | req := &Service1Request{4, 2} 132 | xml, err := rpcRequest2XML("Some.Method", req) 133 | if err != nil { 134 | t.Error("RPC2XML conversion failed", err) 135 | } 136 | 137 | expected := "Some.Method42" 138 | if xml != expected { 139 | t.Error("RPC2XML conversion failed") 140 | t.Error("Expected", expected) 141 | t.Error("Got", xml) 142 | } 143 | 144 | req2 := &Service2Request{"Johnny", 33, true} 145 | xml, err = rpcRequest2XML("Some.Method", req2) 146 | if err != nil { 147 | t.Error("RPC2XML conversion failed", err) 148 | } 149 | 150 | expected = "Some.MethodJohnny331" 151 | if xml != expected { 152 | t.Error("RPC2XML conversion failed") 153 | t.Error("Expected", expected) 154 | t.Error("Got", xml) 155 | } 156 | 157 | address := Address{221, "Baker str.", "London"} 158 | person := Person{"Johnny", "Doe", 33, address} 159 | req3 := &Service3Request{person} 160 | xml, err = rpcRequest2XML("Some.Method", req3) 161 | if err != nil { 162 | t.Error("RPC2XML conversion failed", err) 163 | } 164 | 165 | expected = "Some.MethodNameJohnnySurnameDoeAge33AddressNumber221StreetBaker str.CountryLondon" 166 | if xml != expected { 167 | t.Error("RPC2XML conversion failed") 168 | t.Error("Expected", expected) 169 | t.Error("Got", xml) 170 | } 171 | 172 | res := &Service1Response{42} 173 | xml, err = rpcResponse2XML(res) 174 | if err != nil { 175 | t.Error("RPC2XML conversion failed", err) 176 | } 177 | 178 | expected = "42" 179 | if xml != expected { 180 | t.Error("RPC2XML conversion failed") 181 | t.Error("Expected", expected) 182 | t.Error("Got", xml) 183 | } 184 | } 185 | 186 | func TestServices(t *testing.T) { 187 | s := rpc.NewServer() 188 | s.RegisterCodec(NewCodec(), "text/xml") 189 | s.RegisterService(new(Service1), "") 190 | s.RegisterService(new(Service2), "") 191 | s.RegisterService(new(Service3), "") 192 | 193 | var res Service1Response 194 | if err := execute(t, s, "Service1.Multiply", &Service1Request{4, 2}, &res); err != nil { 195 | t.Error("Expected err to be nil, but got:", err) 196 | } 197 | if res.Result != 8 { 198 | t.Errorf("Wrong response: %v.", res.Result) 199 | } 200 | 201 | var res2 Service2Response 202 | if err := execute(t, s, "Service2.GetGreeting", &Service2Request{"Johnny", 33, true}, &res2); err != nil { 203 | t.Error("Expected err to be nil, but got:", err) 204 | } 205 | if res2.Message != "Hello, user Johnny. You're 33 years old :-P And you has permit." { 206 | t.Errorf("Wrong response: %v.", res2.Message) 207 | } 208 | if res2.Status != 42 { 209 | t.Errorf("Wrong response: %v.", res2.Status) 210 | } 211 | 212 | var res3 Service3Response 213 | address := Address{221, "Baker str.", "London"} 214 | person := Person{"Johnny", "Doe", 33, address} 215 | if err := execute(t, s, "Service3.GetInfo", &Service3Request{person}, &res3); err != nil { 216 | t.Error("Expected err to be nil, but got:", err) 217 | } 218 | 219 | if res3.Info.Phone != "+55-555-555-55-55" { 220 | t.Errorf("Wrong response: %v.", res3.Info) 221 | } 222 | } 223 | --------------------------------------------------------------------------------