Avec un parser soap un peu plus modulaire et un debut de ControlHandler sur les services qui fait des choses
This commit is contained in:
89
soap/buildsoap.go
Normal file
89
soap/buildsoap.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package soap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ----- Générateurs -----
|
||||
|
||||
// BuildUPnPResponse construit une réponse SOAP avec <ActionNameResponse>
|
||||
func BuildUPnPResponse(serviceURN, action string, values map[string]string) ([]byte, error) {
|
||||
env := &Envelope{
|
||||
XMLName: xml.Name{Local: "s:Envelope"},
|
||||
Body: Body{
|
||||
Content: buildActionResponse(serviceURN, action, values),
|
||||
},
|
||||
}
|
||||
|
||||
return marshalSOAP(env)
|
||||
}
|
||||
|
||||
// BuildSOAPFault construit un Fault SOAP standard
|
||||
func BuildSOAPFault(code, description, detail string) ([]byte, error) {
|
||||
env := &Envelope{
|
||||
XMLName: xml.Name{Local: "s:Envelope"},
|
||||
Body: Body{
|
||||
Content: buildFault(code, description, detail),
|
||||
},
|
||||
}
|
||||
|
||||
return marshalSOAP(env)
|
||||
}
|
||||
|
||||
// ----- Internes -----
|
||||
|
||||
func buildActionResponse(serviceURN, action string, values map[string]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf(`<u:%sResponse xmlns:u="%s">`, action, serviceURN))
|
||||
for k, v := range values {
|
||||
buf.WriteString(fmt.Sprintf("<%s>%s</%s>", k, xmlEscape(v), k))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf(`</u:%sResponse>`, action))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func buildFault(code, description, detail string) []byte {
|
||||
return []byte(fmt.Sprintf(`
|
||||
<Fault>
|
||||
<faultcode>%s</faultcode>
|
||||
<faultstring>%s</faultstring>
|
||||
<detail>%s</detail>
|
||||
</Fault>`, xmlEscape(code), xmlEscape(description), xmlEscape(detail)))
|
||||
}
|
||||
|
||||
func marshalSOAP(env *Envelope) ([]byte, error) {
|
||||
type soapEnvelope struct {
|
||||
XMLName xml.Name `xml:"s:Envelope"`
|
||||
SoapNS string `xml:"xmlns:s,attr"`
|
||||
EncNS string `xml:"s:encodingStyle,attr"`
|
||||
Body struct {
|
||||
XMLName xml.Name `xml:"s:Body"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
}
|
||||
|
||||
tmp := soapEnvelope{
|
||||
SoapNS: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
EncNS: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
}
|
||||
tmp.Body.Content = string(env.Body.Content)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
|
||||
enc := xml.NewEncoder(&buf)
|
||||
enc.Indent("", " ")
|
||||
if err := enc.Encode(tmp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc.Flush()
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// xmlEscape échappe manuellement les caractères dangereux
|
||||
func xmlEscape(s string) string {
|
||||
var buf bytes.Buffer
|
||||
xml.EscapeText(&buf, []byte(s))
|
||||
return buf.String()
|
||||
}
|
||||
@@ -3,84 +3,211 @@ package soap
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/didl"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ----- SOAP envelope -----
|
||||
|
||||
type Envelope struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
|
||||
Header *Header `xml:"Header"`
|
||||
Body Body `xml:"Body"`
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Content []byte `xml:",innerxml"` // <- capture tout le contenu du Body sous forme de XML brut
|
||||
type Header struct {
|
||||
Content []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Content []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// ----- UPnP request/response -----
|
||||
|
||||
type ActionRequest struct {
|
||||
Name string
|
||||
Args map[string]interface{}
|
||||
RawXML []byte
|
||||
}
|
||||
|
||||
type ActionResponse struct {
|
||||
Name string
|
||||
Values map[string]string
|
||||
RawXML []byte
|
||||
}
|
||||
|
||||
type Fault struct {
|
||||
Code string
|
||||
Description string
|
||||
Detail string
|
||||
RawXML []byte
|
||||
}
|
||||
|
||||
// ----- Utils -----
|
||||
|
||||
func prettyPrintXML(raw string) string {
|
||||
var out bytes.Buffer
|
||||
decoder := xml.NewDecoder(bytes.NewReader([]byte(raw)))
|
||||
encoder := xml.NewEncoder(&out)
|
||||
encoder.Indent("", " ") // définit l'indentation
|
||||
dec := xml.NewDecoder(bytes.NewReader([]byte(raw)))
|
||||
enc := xml.NewEncoder(&out)
|
||||
enc.Indent("", " ")
|
||||
for {
|
||||
t, err := decoder.Token()
|
||||
t, err := dec.Token()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if err := encoder.EncodeToken(t); err != nil {
|
||||
if err := enc.EncodeToken(t); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
encoder.Flush()
|
||||
enc.Flush()
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func ParseSOAPGeneric(body []byte) {
|
||||
// ----- Parseurs -----
|
||||
|
||||
func ParseSOAPEnvelope(body []byte) (*Envelope, error) {
|
||||
var env Envelope
|
||||
if err := xml.Unmarshal(body, &env); err != nil {
|
||||
log.Warnf("❌ Failed to unmarshal SOAP Envelope: %v", err)
|
||||
return
|
||||
return nil, fmt.Errorf("unmarshal SOAP Envelope: %w", err)
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(bytes.NewReader(env.Body.Content))
|
||||
// ParamDecoder permet de transformer les valeurs des paramètres et éventuellement renommer le paramètre
|
||||
type ParamDecoder func(action, param, value string) (newParam string, out interface{}, err error)
|
||||
|
||||
// ParseUPnPAction extrait l’action et ses arguments à partir d’un Body SOAP.
|
||||
// Si decoder != nil, il est appelé pour chaque paramètre.
|
||||
func ParseUPnPAction(env *Envelope, decoder ParamDecoder) (*ActionRequest, error) {
|
||||
dec := xml.NewDecoder(bytes.NewReader(env.Body.Content))
|
||||
var currentAction string
|
||||
args := make(map[string]interface{})
|
||||
|
||||
for {
|
||||
tok, err := decoder.Token()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Warnf("❌ SOAP parse error: %v", err)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
break
|
||||
return nil, fmt.Errorf("SOAP parse error: %w", err)
|
||||
}
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if currentAction == "" {
|
||||
currentAction = t.Name.Local // nom de l'action
|
||||
currentAction = t.Name.Local
|
||||
} else {
|
||||
var value string
|
||||
decoder.DecodeElement(&value, &t)
|
||||
|
||||
var ival interface{}
|
||||
ival, err = didl.Parse(value)
|
||||
|
||||
if err == nil {
|
||||
ival = ival.(*didl.DIDLLite).ToMarkdown()
|
||||
} else {
|
||||
ival = prettyPrintXML(value)
|
||||
if err := dec.DecodeElement(&value, &t); err != nil {
|
||||
return nil, fmt.Errorf("decode param %s: %w", t.Name.Local, err)
|
||||
}
|
||||
|
||||
args[t.Name.Local] = ival
|
||||
paramName := t.Name.Local
|
||||
var paramValue interface{} = value
|
||||
|
||||
if decoder != nil {
|
||||
if newName, out, err := decoder(currentAction, paramName, value); err == nil {
|
||||
paramName = newName
|
||||
paramValue = out
|
||||
}
|
||||
}
|
||||
|
||||
args[paramName] = paramValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("📡 SOAP Action: %s", currentAction)
|
||||
for k, v := range args {
|
||||
log.Infof(" %s = %v", k, v)
|
||||
return &ActionRequest{
|
||||
Name: currentAction,
|
||||
Args: args,
|
||||
RawXML: env.Body.Content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Response (renderer -> contrôleur)
|
||||
func ParseUPnPResponse(env *Envelope) (*ActionResponse, *Fault, error) {
|
||||
dec := xml.NewDecoder(bytes.NewReader(env.Body.Content))
|
||||
var respName string
|
||||
values := make(map[string]string)
|
||||
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, nil, fmt.Errorf("SOAP parse error: %w", err)
|
||||
}
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if t.Name.Local == "Fault" {
|
||||
// Parse fautes SOAP
|
||||
var f struct {
|
||||
Code string `xml:"faultcode"`
|
||||
Desc string `xml:"faultstring"`
|
||||
Detail string `xml:"detail"`
|
||||
}
|
||||
if err := dec.DecodeElement(&f, &t); err != nil {
|
||||
return nil, nil, fmt.Errorf("decode Fault: %w", err)
|
||||
}
|
||||
return nil, &Fault{
|
||||
Code: f.Code,
|
||||
Description: f.Desc,
|
||||
Detail: f.Detail,
|
||||
RawXML: env.Body.Content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if respName == "" {
|
||||
respName = t.Name.Local
|
||||
} else {
|
||||
var value string
|
||||
if err := dec.DecodeElement(&value, &t); err != nil {
|
||||
return nil, nil, fmt.Errorf("decode response param %s: %w", t.Name.Local, err)
|
||||
}
|
||||
values[t.Name.Local] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if respName == "" {
|
||||
return nil, nil, fmt.Errorf("no response or fault in SOAP body")
|
||||
}
|
||||
|
||||
return &ActionResponse{respName, values, env.Body.Content}, nil, nil
|
||||
}
|
||||
|
||||
// ----- Exemple -----
|
||||
|
||||
func ParseSOAPGeneric(body []byte, decoder ParamDecoder) {
|
||||
env, err := ParseSOAPEnvelope(body)
|
||||
if err != nil {
|
||||
log.Warnf("❌ %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Essayer d'abord comme requête
|
||||
if req, err := ParseUPnPAction(env, decoder); err == nil && req.Name != "" {
|
||||
log.Infof("📡 SOAP Request Action: %s", req.Name)
|
||||
for k, v := range req.Args {
|
||||
log.Infof(" %s = %v", k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sinon comme réponse
|
||||
if resp, fault, err := ParseUPnPResponse(env); err == nil {
|
||||
if resp != nil {
|
||||
log.Infof("📡 SOAP Response: %s", resp.Name)
|
||||
for k, v := range resp.Values {
|
||||
log.Infof(" %s = %v", k, v)
|
||||
}
|
||||
} else if fault != nil {
|
||||
log.Warnf("❌ SOAP Fault: %s - %s (detail: %s)", fault.Code, fault.Description, fault.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user