Files
pmomusic/upnp/serviceinstance.go

234 lines
6.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package upnp
import (
"fmt"
"io"
"net/http"
"gargoton.petite-maison-orange.fr/eric/pmomusic/soap"
"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"
log "github.com/sirupsen/logrus"
)
type ServiceInstance struct {
name string
identifier string
version int
device *DeviceInstance
statevariables statevariables.StateVarInstanceSet
actions actions.ActionInstanceSet
}
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),
)
mux.HandleFunc(
svc.ControlURL(),
svc.ControlHandler(),
)
mux.HandleFunc(
svc.EventSubURL(),
svc.EventSubHandler(),
)
log.Infof(
"✅ Service description for %s:%s available at : %s%s",
svc.device.Name(),
svc.Name(),
svc.device.server.BaseURL(),
svc.SCPDURL(),
)
return nil
}
func (svc *ServiceInstance) USN() string {
return fmt.Sprintf("uuid:%s::urn:%s", svc.device.UDN(), svc.ServiceType())
}
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
}
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
}
func (svc *ServiceInstance) EventSubHandler() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
log.Infof("Event Subscription handler for %s:%s", svc.device.Name(), svc.Name())
// corps vide volontairement
}
}
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
}
// 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]
log.Debugf("Look for a variable named : %s -> %v", param, ok)
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
}
log.Info(req.ToMarkdown())
// 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{})
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(resp)
}
}