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:
2025-09-07 11:45:43 +02:00
parent 437d9db4b7
commit a04f8cce6b
9 changed files with 368 additions and 42 deletions

89
soap/buildsoap.go Normal file
View 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()
}

View File

@@ -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 laction et ses arguments à partir dun 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)
}
}
}