2025-09-06 17:45:28 +02:00
|
|
|
|
package upnp
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-09-07 16:17:24 +02:00
|
|
|
|
"context"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
"fmt"
|
2025-09-07 07:59:26 +02:00
|
|
|
|
"io"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
"net/http"
|
2025-09-07 16:17:24 +02:00
|
|
|
|
"net/url"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
|
2025-09-07 07:59:26 +02:00
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/soap"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
|
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
|
|
|
|
|
"github.com/beevik/etree"
|
2025-09-07 16:17:24 +02:00
|
|
|
|
"github.com/google/uuid"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type ServiceInstance struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
identifier string
|
|
|
|
|
|
version int
|
|
|
|
|
|
|
|
|
|
|
|
device *DeviceInstance
|
|
|
|
|
|
statevariables statevariables.StateVarInstanceSet
|
|
|
|
|
|
actions actions.ActionInstanceSet
|
2025-09-07 16:17:24 +02:00
|
|
|
|
subscribers map[string]string // SID → Callback URL
|
|
|
|
|
|
changedBuffer map[string]interface{}
|
|
|
|
|
|
seqid map[string]uint32
|
|
|
|
|
|
cbmu sync.Mutex
|
|
|
|
|
|
mu sync.Mutex
|
2025-09-06 17:45:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 16:17:24 +02:00
|
|
|
|
const (
|
|
|
|
|
|
MethodSubscribe = "SUBSCRIBE"
|
|
|
|
|
|
MethodUnsubscribe = "UNSUBSCRIBE"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-06 17:45:28 +02:00
|
|
|
|
func (si *ServiceInstance) Name() string {
|
|
|
|
|
|
return si.name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) TypeID() string {
|
|
|
|
|
|
return "ServiceInstance"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (si *ServiceInstance) Identifier() string {
|
|
|
|
|
|
return si.identifier
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) ServiceType() string {
|
|
|
|
|
|
return fmt.Sprintf("urn:schemas-upnp-org:service:%s:%d", svc.name, svc.version)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) ServiceId() string {
|
|
|
|
|
|
return fmt.Sprintf("urn:upnp-org:serviceId:%s", svc.identifier)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) BaseRoute() string {
|
|
|
|
|
|
return fmt.Sprintf("%s/service/%s", svc.device.BaseRoute(), svc.Name())
|
|
|
|
|
|
}
|
|
|
|
|
|
func (svc *ServiceInstance) ControlURL() string {
|
|
|
|
|
|
return fmt.Sprintf("%s/control", svc.BaseRoute())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) EventSubURL() string {
|
|
|
|
|
|
return fmt.Sprintf("%s/event", svc.BaseRoute())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) SCPDURL() string {
|
|
|
|
|
|
return fmt.Sprintf("%s/desc.xml", svc.BaseRoute())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) RegisterURLs() error {
|
|
|
|
|
|
|
|
|
|
|
|
mux, ok := svc.device.server.httpSrv.Handler.(*http.ServeMux)
|
|
|
|
|
|
|
|
|
|
|
|
if mux == nil || !ok {
|
|
|
|
|
|
return fmt.Errorf("❌ Device %s the server handler is not correctly defined", svc.Name())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mux.HandleFunc(
|
|
|
|
|
|
svc.SCPDURL(),
|
|
|
|
|
|
svc.device.server.ServeXML(svc.SPCDElement),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-07 07:59:26 +02:00
|
|
|
|
mux.HandleFunc(
|
|
|
|
|
|
svc.ControlURL(),
|
|
|
|
|
|
svc.ControlHandler(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
mux.HandleFunc(
|
|
|
|
|
|
svc.EventSubURL(),
|
|
|
|
|
|
svc.EventSubHandler(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-06 17:45:28 +02:00
|
|
|
|
log.Infof(
|
|
|
|
|
|
"✅ Service description for %s:%s available at : %s%s",
|
|
|
|
|
|
svc.device.Name(),
|
|
|
|
|
|
svc.Name(),
|
|
|
|
|
|
svc.device.server.BaseURL(),
|
|
|
|
|
|
svc.SCPDURL(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 07:59:26 +02:00
|
|
|
|
func (svc *ServiceInstance) USN() string {
|
|
|
|
|
|
return fmt.Sprintf("uuid:%s::urn:%s", svc.device.UDN(), svc.ServiceType())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 17:45:28 +02:00
|
|
|
|
func (svc *ServiceInstance) SPCDElement() *etree.Element {
|
|
|
|
|
|
elem := etree.NewElement("scpd")
|
|
|
|
|
|
|
|
|
|
|
|
elem.CreateAttr("xmlns", "urn:schemas-upnp-org:service-1-0")
|
|
|
|
|
|
|
|
|
|
|
|
spec := elem.CreateElement("specVersion")
|
|
|
|
|
|
spec.CreateElement("major").SetText("1")
|
|
|
|
|
|
spec.CreateElement("minor").SetText("0")
|
|
|
|
|
|
|
|
|
|
|
|
if len(svc.actions) > 0 {
|
|
|
|
|
|
elem.AddChild(svc.actions.ToXMLElement())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(svc.statevariables) > 0 {
|
|
|
|
|
|
elem.AddChild(svc.statevariables.ToXMLElement())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return elem
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 16:17:24 +02:00
|
|
|
|
func (svc *ServiceInstance) AddSubscriber(sid, callback string) {
|
|
|
|
|
|
svc.mu.Lock()
|
|
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
if svc.subscribers == nil {
|
|
|
|
|
|
svc.subscribers = make(map[string]string)
|
|
|
|
|
|
}
|
|
|
|
|
|
svc.subscribers[sid] = callback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) RenewSubscriber(sid, timeout string) {
|
|
|
|
|
|
// Pour l'instant juste log, on peut étendre avec expiration
|
|
|
|
|
|
svc.mu.Lock()
|
|
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
log.Infof("Renewed SID %s for timeout %s", sid, timeout)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) RemoveSubscriber(sid string) {
|
|
|
|
|
|
svc.mu.Lock()
|
|
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
delete(svc.subscribers, sid)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Envoi d'un événement initial (optionnel)
|
|
|
|
|
|
func (svc *ServiceInstance) SendInitialEvent(sid string) {
|
|
|
|
|
|
svc.mu.Lock()
|
|
|
|
|
|
callback := svc.subscribers[sid]
|
|
|
|
|
|
svc.mu.Unlock()
|
|
|
|
|
|
if callback == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ici tu peux construire un SOAP Event XML minimal
|
|
|
|
|
|
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><e:property><Status>Initial</Status></e:property></e:propertyset>`
|
|
|
|
|
|
|
|
|
|
|
|
callback = strings.TrimSpace(callback)
|
|
|
|
|
|
callback = strings.Trim(callback, "<>") // retire les < et >
|
|
|
|
|
|
|
|
|
|
|
|
// parser pour valider
|
|
|
|
|
|
u, err := url.Parse(callback)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Invalid callback URL: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("NOTIFY", u.String(), strings.NewReader(body))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Failed to create NOTIFY request: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "text/xml; charset=\"utf-8\"")
|
|
|
|
|
|
req.Header.Set("NT", "upnp:event")
|
|
|
|
|
|
req.Header.Set("NTS", "upnp:propchange")
|
|
|
|
|
|
req.Header.Set("SID", sid)
|
|
|
|
|
|
req.Header.Set("SEQ", "0")
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Failed to send initial event to %s: %v", callback, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
log.Infof("✅ Initial event sent to %s, status=%s", callback, resp.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 17:45:28 +02:00
|
|
|
|
func (svc *ServiceInstance) ToXMLElement() *etree.Element {
|
|
|
|
|
|
elem := etree.NewElement("service")
|
|
|
|
|
|
|
|
|
|
|
|
st := elem.CreateElement("serviceType")
|
|
|
|
|
|
st.SetText(svc.ServiceType())
|
|
|
|
|
|
|
|
|
|
|
|
sid := elem.CreateElement("serviceId")
|
|
|
|
|
|
sid.SetText(svc.ServiceId())
|
|
|
|
|
|
|
|
|
|
|
|
spcd := elem.CreateElement("SCPDURL")
|
|
|
|
|
|
spcd.SetText(svc.SCPDURL())
|
|
|
|
|
|
|
|
|
|
|
|
ctrl := elem.CreateElement("controlURL")
|
|
|
|
|
|
ctrl.SetText(svc.ControlURL())
|
|
|
|
|
|
|
|
|
|
|
|
event := elem.CreateElement("eventSubURL")
|
|
|
|
|
|
event.SetText(svc.EventSubURL())
|
|
|
|
|
|
|
|
|
|
|
|
return elem
|
|
|
|
|
|
}
|
2025-09-07 07:59:26 +02:00
|
|
|
|
|
2025-09-07 16:17:24 +02:00
|
|
|
|
func (svc *ServiceInstance) EventToBeSent(name string, value interface{}) {
|
|
|
|
|
|
svc.cbmu.Lock()
|
|
|
|
|
|
defer svc.cbmu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
svc.changedBuffer[name] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) nextSeq(sid string) string {
|
|
|
|
|
|
svc.mu.Lock()
|
|
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
svc.seqid[sid]++
|
|
|
|
|
|
return fmt.Sprintf("%d", svc.seqid[sid])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) NotifySubscribers() {
|
|
|
|
|
|
svc.cbmu.Lock()
|
|
|
|
|
|
if len(svc.subscribers) == 0 || len(svc.changedBuffer) == 0 {
|
|
|
|
|
|
svc.cbmu.Unlock()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Copier et réinitialiser le buffer
|
|
|
|
|
|
changed := svc.changedBuffer
|
|
|
|
|
|
svc.changedBuffer = make(map[string]interface{})
|
|
|
|
|
|
svc.cbmu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
for sid, callback := range svc.subscribers {
|
|
|
|
|
|
go func(sid, callback string, changed map[string]interface{}) {
|
|
|
|
|
|
callback = strings.TrimSpace(callback)
|
|
|
|
|
|
callback = strings.Trim(callback, "<>")
|
|
|
|
|
|
|
|
|
|
|
|
u, err := url.Parse(callback)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Invalid callback URL %s: %v", callback, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">`
|
|
|
|
|
|
for name, val := range changed {
|
|
|
|
|
|
body += fmt.Sprintf("<e:property><%s>%v</%s></e:property>", name, val, name)
|
|
|
|
|
|
}
|
|
|
|
|
|
body += "</e:propertyset>"
|
|
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("NOTIFY", u.String(), strings.NewReader(body))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Failed to create NOTIFY request to %s: %v", callback, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", `text/xml; charset="utf-8"`)
|
|
|
|
|
|
req.Header.Set("NT", "upnp:event")
|
|
|
|
|
|
req.Header.Set("NTS", "upnp:propchange")
|
|
|
|
|
|
req.Header.Set("SID", sid)
|
|
|
|
|
|
req.Header.Set("SEQ", svc.nextSeq(sid))
|
|
|
|
|
|
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("Failed to notify subscriber %s: %v", callback, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
|
|
log.Infof("✅ Notified subscriber %s of changes: %v", callback, changed)
|
|
|
|
|
|
}(sid, callback, changed)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) StartNotifier(ctx context.Context, interval time.Duration) {
|
|
|
|
|
|
log.Infof("✅ Starting notifier for %s:%s every %.2f s", svc.device.Name(), svc.Name(), interval.Seconds())
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
ticker := time.NewTicker(interval)
|
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
|
log.Infof("✅ Notifier stopped for %s:%s", svc.device.Name(), svc.Name())
|
|
|
|
|
|
return
|
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
|
svc.NotifySubscribers()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 07:59:26 +02:00
|
|
|
|
func (svc *ServiceInstance) EventSubHandler() func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2025-09-07 16:17:24 +02:00
|
|
|
|
log.Infof("📡 Event Subscription request for %s:%s", svc.device.Name(), svc.Name())
|
|
|
|
|
|
|
|
|
|
|
|
sid := r.Header.Get("SID")
|
|
|
|
|
|
timeout := r.Header.Get("Timeout")
|
|
|
|
|
|
callback := r.Header.Get("Callback")
|
|
|
|
|
|
|
|
|
|
|
|
switch r.Method {
|
|
|
|
|
|
case MethodSubscribe:
|
|
|
|
|
|
if sid == "" {
|
|
|
|
|
|
// Nouvelle subscription
|
|
|
|
|
|
sid = fmt.Sprintf("uuid:%s", uuid.New().String())
|
|
|
|
|
|
if callback != "" {
|
|
|
|
|
|
svc.AddSubscriber(sid, callback)
|
|
|
|
|
|
}
|
|
|
|
|
|
if timeout == "" {
|
|
|
|
|
|
timeout = "Second-1800"
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Infof("🔔 New subscription: SID=%s, Callback=%s, Timeout=%s", sid, callback, timeout)
|
|
|
|
|
|
go svc.SendInitialEvent(sid) // envoyer l’état initial
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Renouvellement
|
|
|
|
|
|
svc.RenewSubscriber(sid, timeout)
|
|
|
|
|
|
log.Infof("♻️ Renew subscription: SID=%s, Timeout=%s", sid, timeout)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("SID", sid)
|
|
|
|
|
|
w.Header().Set("Timeout", timeout)
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
|
|
|
|
case MethodUnsubscribe:
|
|
|
|
|
|
if sid != "" {
|
|
|
|
|
|
svc.RemoveSubscriber(sid)
|
|
|
|
|
|
log.Infof("❌ Unsubscribe SID=%s", sid)
|
|
|
|
|
|
}
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
log.Warnf("Unsupported EventSub method: %s", r.Method)
|
|
|
|
|
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Debug log headers
|
|
|
|
|
|
for k, v := range r.Header {
|
|
|
|
|
|
log.Debugf("Header: %s=%v", k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Lire le body au besoin
|
|
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
|
|
|
|
if err == nil && len(body) > 0 {
|
|
|
|
|
|
log.Debugf("Body: %s", string(body))
|
|
|
|
|
|
}
|
2025-09-07 07:59:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *ServiceInstance) ControlHandler() func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
log.Infof("📡 Control request for %s:%s", svc.device.Name(), svc.Name())
|
|
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("❌ Failed to read body: %v", err)
|
|
|
|
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 11:45:43 +02:00
|
|
|
|
// Callback générique pour décoder chaque paramètre
|
|
|
|
|
|
decoder := func(action, param, value string) (string, interface{}, error) {
|
|
|
|
|
|
// 1️⃣ Chercher si le param correspond à une StateVarInstance
|
|
|
|
|
|
|
|
|
|
|
|
sv, ok := svc.statevariables[param]
|
2025-09-07 12:49:25 +02:00
|
|
|
|
log.Debugf("Look for a variable named : %s -> %v", param, ok)
|
2025-09-07 11:45:43 +02:00
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
if sv.HasParser() {
|
|
|
|
|
|
v, err := sv.ParseValue(value)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return param, value, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return param, v, err
|
|
|
|
|
|
}
|
|
|
|
|
|
v, err := sv.Cast(value)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return param, value, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return param, v, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ Chercher si le param correspond à une ActionInstance et appliquer un parseur associé
|
|
|
|
|
|
act, ok := svc.actions[action]
|
|
|
|
|
|
log.Debugf("Look for an action named : %s -> %v", action, ok)
|
|
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
if argument, ok := act.Arguments(param); ok {
|
|
|
|
|
|
sv_name := argument.StateVariable().Name()
|
|
|
|
|
|
sv := svc.statevariables[sv_name]
|
|
|
|
|
|
v, err := sv.ParseValue(value)
|
|
|
|
|
|
|
|
|
|
|
|
log.Debugf("It corresponds to variable : %s with a parser %v", sv_name, sv.HasParser())
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return param, value, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return param, v, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ Sinon, valeur brute
|
|
|
|
|
|
return param, value, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
env, err := soap.ParseSOAPEnvelope(body)
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("❌ Failed to parse SOAP enveloppe: %v", err)
|
|
|
|
|
|
soapResp, _ := soap.BuildSOAPFault("s:Client", "Invalid Args", err.Error())
|
|
|
|
|
|
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
|
_, _ = w.Write(soapResp)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req, err := soap.ParseUPnPAction(env, decoder)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Errorf("❌ Failed to parse SOAP Action: %v", err)
|
|
|
|
|
|
soapResp, _ := soap.BuildSOAPFault("s:Client", "Invalid Args", err.Error())
|
|
|
|
|
|
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
|
_, _ = w.Write(soapResp)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 12:49:25 +02:00
|
|
|
|
log.Info(req.ToMarkdown())
|
2025-09-07 07:59:26 +02:00
|
|
|
|
|
2025-09-07 11:45:43 +02:00
|
|
|
|
// Ici tu peux appeler l'action correspondante sur svc.actions[req.Name] et récupérer le résultat
|
|
|
|
|
|
// Exemple de réponse minimale :
|
|
|
|
|
|
resp, _ := soap.BuildUPnPResponse(svc.ServiceType(), req.Name, map[string]string{})
|
2025-09-07 07:59:26 +02:00
|
|
|
|
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
2025-09-07 11:45:43 +02:00
|
|
|
|
_, _ = w.Write(resp)
|
2025-09-07 07:59:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|