234 lines
6.0 KiB
Go
234 lines
6.0 KiB
Go
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)
|
||
}
|
||
}
|