From 6f5b9d46a0a0d8f4c644de44e9c595336dc4c583 Mon Sep 17 00:00:00 2001 From: Jan Halfar Date: Tue, 2 Feb 2016 18:20:31 +0100 Subject: [PATCH] added custom marshalling, verbosity and cleaned up --- README.md | 66 +++++++++++++-- client.go | 140 +++++++++++++++++++++++++++++++ examples/client.go | 30 +++++++ examples/simple-server.go | 52 ++++++++++++ server.go | 171 +++++++++++++++++++++++++++++++------- soap.go | 27 +++--- test/test.go | 42 ---------- 7 files changed, 439 insertions(+), 89 deletions(-) create mode 100644 client.go create mode 100644 examples/client.go create mode 100644 examples/simple-server.go delete mode 100644 test/test.go diff --git a/README.md b/README.md index e98a316..c30e518 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,45 @@ First of all do not write SOAP services if you can avoid it! It is over. If you can not avoid it this package might help. +## Service + ```go package main -import "github.com/foomo/soap" +import ( + "encoding/xml" + "fmt" + "net/http" + "github.com/foomo/soap" +) + +// FooRequest a simple request type FooRequest struct { - Foo string + XMLName xml.Name `xml:"fooRequest"` + Foo string } +// FooResponse a simple response type FooResponse struct { Bar string } +// RunServer run a little demo server func RunServer() { - soapServer := soap.NewServer("127.0.0.1:8080") - + soapServer := soap.NewServer() soapServer.HandleOperation( + // SOAPAction "operationFoo", + // tagname of soap body content + "fooRequest", + // RequestFactoryFunc - give the server sth. to unmarshal the request into func() interface{} { return &FooRequest{} }, - func(request interface{}) (response interface{}, err error) { - fooRequest := request.(FooRequest) + // OperationHandlerFunc - do something + func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) { + fooRequest := request.(*FooRequest) fooResponse := &FooResponse{ Bar: "Hello " + fooRequest.Foo, } @@ -34,11 +50,47 @@ func RunServer() { return }, ) - + err := soapServer.ListenAndServe(":8080") + fmt.Println("exiting with error", err) } func main() { RunServer() } +``` + +## Client + +```go +package main + +import ( + "encoding/xml" + "log" + + "github.com/foomo/soap" +) + +// FooRequest a simple request +type FooRequest struct { + XMLName xml.Name `xml:"fooRequest"` + Foo string +} + +// FooResponse a simple response +type FooResponse struct { + Bar string +} + +func main() { + soap.Verbose = true + client := soap.NewClient("http://127.0.0.1:8080/", nil, nil) + response := &FooResponse{} + httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response) + if err != nil { + panic(err) + } + log.Println(response.Bar, httpResponse.Status) +} ``` diff --git a/client.go b/client.go new file mode 100644 index 0000000..b6147ac --- /dev/null +++ b/client.go @@ -0,0 +1,140 @@ +package soap + +import ( + "bytes" + "encoding/xml" + "io/ioutil" + "log" + "net" + "net/http" + "time" +) + +// ClientDialTimeout default timeout 30s +var ClientDialTimeout = time.Duration(30 * time.Second) + +// UserAgent is the default user agent +var UserAgent = "go-soap-0.1" + +// Verbose be verbose +var Verbose = false + +func l(m ...interface{}) { + if Verbose { + log.Println(m...) + } +} + +// XMLMarshaller lets you inject your favourite custom xml implementation +type XMLMarshaller interface { + Marshal(v interface{}) ([]byte, error) + Unmarshal(xml []byte, v interface{}) error +} + +type defaultMarshaller struct { +} + +func (dm *defaultMarshaller) Marshal(v interface{}) (xmlBytes []byte, err error) { + return xml.Marshal(v) +} + +func (dm *defaultMarshaller) Unmarshal(xmlBytes []byte, v interface{}) error { + return xml.Unmarshal(xmlBytes, v) +} + +func newDefaultMarshaller() XMLMarshaller { + return &defaultMarshaller{} +} + +func dialTimeout(network, addr string) (net.Conn, error) { + return net.DialTimeout(network, addr, ClientDialTimeout) +} + +// BasicAuth credentials for the client +type BasicAuth struct { + Login string + Password string +} + +// Client generic SOAP client +type Client struct { + url string + tls bool + auth *BasicAuth + tr *http.Transport + Marshaller XMLMarshaller +} + +// NewClient constructor +func NewClient(url string, auth *BasicAuth, tr *http.Transport) *Client { + return &Client{ + url: url, + auth: auth, + tr: tr, + Marshaller: newDefaultMarshaller(), + } +} + +// Call make a SOAP call +func (s *Client) Call(soapAction string, request, response interface{}) (httpResponse *http.Response, err error) { + envelope := Envelope{} + + envelope.Body.Content = request + + xmlBytes, err := s.Marshaller.Marshal(envelope) + if err != nil { + return + } + + req, err := http.NewRequest("POST", s.url, bytes.NewBuffer(xmlBytes)) + if err != nil { + return + } + if s.auth != nil { + req.SetBasicAuth(s.auth.Login, s.auth.Password) + } + + req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"") + req.Header.Set("User-Agent", UserAgent) + + if soapAction != "" { + req.Header.Add("SOAPAction", soapAction) + } + + req.Close = true + tr := s.tr + if tr == nil { + tr = http.DefaultTransport.(*http.Transport) + } + client := &http.Client{Transport: tr} + l("POST to", s.url, "with", string(xmlBytes)) + httpResponse, err = client.Do(req) + if err != nil { + return + } + defer httpResponse.Body.Close() + + rawbody, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return + } + if len(rawbody) == 0 { + l("empty response") + return + } + + l("response", string(rawbody)) + respEnvelope := new(Envelope) + respEnvelope.Body = Body{Content: response} + err = xml.Unmarshal(rawbody, respEnvelope) + if err != nil { + return + } + + fault := respEnvelope.Body.Fault + if fault != nil { + err = fault + return + } + return +} diff --git a/examples/client.go b/examples/client.go new file mode 100644 index 0000000..0f871d8 --- /dev/null +++ b/examples/client.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/xml" + "log" + + "github.com/foomo/soap" +) + +// FooRequest a simple request +type FooRequest struct { + XMLName xml.Name `xml:"fooRequest"` + Foo string +} + +// FooResponse a simple response +type FooResponse struct { + Bar string +} + +func main() { + soap.Verbose = true + client := soap.NewClient("http://127.0.0.1:8080/", nil, nil) + response := &FooResponse{} + httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response) + if err != nil { + panic(err) + } + log.Println(response.Bar, httpResponse.Status) +} diff --git a/examples/simple-server.go b/examples/simple-server.go new file mode 100644 index 0000000..c00b3f2 --- /dev/null +++ b/examples/simple-server.go @@ -0,0 +1,52 @@ +package main + +import ( + "encoding/xml" + "fmt" + "net/http" + + "github.com/foomo/soap" +) + +// FooRequest a simple request +type FooRequest struct { + XMLName xml.Name `xml:"fooRequest"` + Foo string +} + +// FooResponse a simple response +type FooResponse struct { + Bar string +} + +// RunServer run a little demo server +func RunServer() { + soapServer := soap.NewServer() + soapServer.HandleOperation( + // SOAPAction + "operationFoo", + // tagname of soap body content + "fooRequest", + // RequestFactoryFunc - give the server sth. to unmarshal the request into + func() interface{} { + return &FooRequest{} + }, + // OperationHandlerFunc - do something + func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) { + fooRequest := request.(*FooRequest) + fooResponse := &FooResponse{ + Bar: "Hello \"" + fooRequest.Foo + "\"", + } + response = fooResponse + return + }, + ) + err := soapServer.ListenAndServe(":8080") + fmt.Println("exiting with error", err) +} + +func main() { + // see what is going on + soap.Verbose = true + RunServer() +} diff --git a/server.go b/server.go index a990d2f..fe50194 100644 --- a/server.go +++ b/server.go @@ -1,71 +1,170 @@ package soap import ( + "encoding/json" + "encoding/xml" "errors" + "io/ioutil" "net/http" ) -type OperationHandlerFunc func(request interface{}) (response interface{}, err error) +// OperationHandlerFunc runs the actual business logic - request is whatever you constructed in RequestFactoryFunc +type OperationHandlerFunc func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) + +// RequestFactoryFunc constructs a request object for OperationHandlerFunc type RequestFactoryFunc func() interface{} +type dummyContent struct{} + type operationHander struct { requestFactory RequestFactoryFunc handler OperationHandlerFunc } -type Server struct { - handlers map[string]map[string]*operationHander +type responseWriter struct { + w http.ResponseWriter + outputStarted bool } +func (w *responseWriter) Header() http.Header { + return w.w.Header() +} +func (w *responseWriter) Write(b []byte) (int, error) { + w.outputStarted = true + return w.w.Write(b) +} + +func (w *responseWriter) WriteHeader(code int) { + w.w.WriteHeader(code) +} + +// Server a SOAP server, which can be run standalone or used as a http.HandlerFunc +type Server struct { + handlers map[string]map[string]*operationHander + Marshaller XMLMarshaller +} + +// NewServer construct a new SOAP server func NewServer() *Server { s := &Server{ - handlers: make(map[string]map[string]*operationHander), + handlers: make(map[string]map[string]*operationHander), + Marshaller: newDefaultMarshaller(), } return s } // HandleOperation register to handle an operation func (s *Server) HandleOperation(action string, messageType string, requestFactory RequestFactoryFunc, operationHandlerFunc OperationHandlerFunc) { + _, ok := s.handlers[action] + if !ok { + s.handlers[action] = make(map[string]*operationHander) + } s.handlers[action][messageType] = &operationHander{ handler: operationHandlerFunc, requestFactory: requestFactory, } } -func (s *Server) serveSOAP(requestEnvelopeBytes []byte, soapAction string) (responseEnvelopeBytes []byte, err error) { - messageType := "find me as the element name in the soap body" - actionHandlers, ok := s.handlers[soapAction] - if !ok { - err = errors.New("could not find handlers for action: \"" + soapAction + "\"") - return +func (s *Server) handleError(err error, w http.ResponseWriter) { + // has to write a soap fault + l("handling error:", err) + responseEnvelope := &Envelope{ + Body: Body{ + Content: &Fault{ + String: err.Error(), + }, + }, } - handler, ok := actionHandlers[messageType] - if !ok { - err = errors.New("no handler for message type: " + messageType) - return - } - request := handler.requestFactory() - // parse from envelope.body.content into request - response, err := handler.handler(request) - responseEnvelope := &SOAPEnvelope{ - Body: SOAPBody{}, - } - if err != nil { - // soap fault party time - responseEnvelope.Body.Fault = &SOAPFault{ - String: err.Error(), - } + xmlBytes, xmlErr := s.Marshaller.Marshal(responseEnvelope) + if xmlErr != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("could not marshal soap fault for: " + err.Error() + " xmlError: " + xmlErr.Error())) } else { - responseEnvelope.Body.Content = response + w.Write(xmlBytes) } - // marshal responseEnvelope - return } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { case "POST": - w.Write([]byte("that actually could be a soap request")) + l("incoming POST") + soapRequestBytes, err := ioutil.ReadAll(r.Body) + + if err != nil { + s.handleError(errors.New("could not read POST:: "+err.Error()), w) + return + } + + soapAction := r.Header.Get("SOAPAction") + l("SOAPAction", "\""+soapAction+"\"") + actionHandlers, ok := s.handlers[soapAction] + if !ok { + s.handleError(errors.New("unknown action \""+soapAction+"\""), w) + return + } + + // we need to find out, what is in the body + probeEnvelope := &Envelope{ + Body: Body{ + Content: &dummyContent{}, + }, + } + + err = xml.Unmarshal(soapRequestBytes, &probeEnvelope) + if err != nil { + s.handleError(errors.New("could not probe soap body content:: "+err.Error()), w) + return + } + + t := probeEnvelope.Body.SOAPBodyContentType + l("found content type", t) + actionHandler, ok := actionHandlers[t] + if !ok { + s.handleError(errors.New("no action handler for content type: \""+t+"\""), w) + return + } + request := actionHandler.requestFactory() + envelope := &Envelope{ + Body: Body{ + Content: request, + }, + } + + err = xml.Unmarshal(soapRequestBytes, &envelope) + if err != nil { + s.handleError(errors.New("could not unmarshal request:: "+err.Error()), w) + return + } + l("request", jsonDump(envelope)) + + // we have a valid request time to call the handler + responseWriter := &responseWriter{ + w: w, + outputStarted: false, + } + response, err := actionHandler.handler(request, responseWriter, r) + if err != nil { + l("action handler threw up") + s.handleError(err, w) + return + } + l("result", jsonDump(response)) + if !responseWriter.outputStarted { + responseEnvelope := &Envelope{ + Body: Body{ + Content: response, + }, + } + xmlBytes, err := s.Marshaller.Marshal(responseEnvelope) + if err != nil { + s.handleError(errors.New("could not marshal response:: "+err.Error()), w) + } + w.Write(xmlBytes) + } else { + l("action handler sent its own output") + } + default: // this will be a soap fault !? w.Write([]byte("this is a soap service - you have to POST soap requests\n")) @@ -73,6 +172,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func jsonDump(v interface{}) string { + if !Verbose { + return "not dumping" + } + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "error in json dump :: " + err.Error() + } + return string(jsonBytes) +} + +// ListenAndServe run standalone func (s *Server) ListenAndServe(addr string) error { return http.ListenAndServe(addr, s) } diff --git a/soap.go b/soap.go index 4dab6f7..05635f4 100644 --- a/soap.go +++ b/soap.go @@ -2,26 +2,31 @@ package soap import "encoding/xml" -type SOAPEnvelope struct { +// Envelope type +type Envelope struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` - Body SOAPBody + Body Body } -type SOAPHeader struct { +// Header type +type Header struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"` Header interface{} } -type SOAPBody struct { +// Body type +type Body struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` - Fault *SOAPFault `xml:",omitempty"` - Content interface{} `xml:",omitempty"` + Fault *Fault `xml:",omitempty"` + Content interface{} `xml:",omitempty"` + SOAPBodyContentType string `xml:"-"` } -type SOAPFault struct { +// Fault type +type Fault struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` Code string `xml:"faultcode,omitempty"` @@ -30,7 +35,8 @@ type SOAPFault struct { Detail string `xml:"detail,omitempty"` } -func (b *SOAPBody) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +// UnmarshalXML implement xml.Unmarshaler +func (b *Body) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if b.Content == nil { return xml.UnmarshalError("Content must be a pointer to a struct") } @@ -56,7 +62,7 @@ Loop: if consumed { return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant") } else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" { - b.Fault = &SOAPFault{} + b.Fault = &Fault{} b.Content = nil err = d.DecodeElement(b.Fault, &se) @@ -66,6 +72,7 @@ Loop: consumed = true } else { + b.SOAPBodyContentType = se.Name.Local if err = d.DecodeElement(b.Content, &se); err != nil { return err } @@ -80,6 +87,6 @@ Loop: return nil } -func (f *SOAPFault) Error() string { +func (f *Fault) Error() string { return f.String } diff --git a/test/test.go b/test/test.go deleted file mode 100644 index fe95a9d..0000000 --- a/test/test.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/foomo/soap" -) - -type FooRequest struct { - Foo string -} - -type FooResponse struct { - Bar string -} - -func RunServer() { - soapServer := soap.NewServer() - /* - soapServer.HandleOperation( - "operationFoo", - "FooRequest", - func() interface{} { - return &FooRequest{} - }, - func(request interface{}) (response interface{}, err error) { - fooRequest := request.(*FooRequest) - fooResponse := &FooResponse{ - Bar: "Hello " + fooRequest.Foo, - } - response = fooResponse - return - }, - ) - */ - err := soapServer.ListenAndServe(":8080") - fmt.Println(err) -} - -func main() { - RunServer() -}