package dlna import ( "bytes" "context" "crypto/md5" "encoding/xml" "fmt" "io" "log" "net" "net/http" "net/url" "strconv" "strings" "time" dms_dlna "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/ssdp" "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/events" ) const ( serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" rootDescPath = "/rootDesc.xml" resPath = "/r/" serviceControlURL = "/ctl" ) type DLNAServer struct { ds model.DataStore broker events.Broker ssdp SSDPServer } type SSDPServer struct { // The service SOAP handler keyed by service URN. services map[string]UPnPService Interfaces []net.Interface HTTPConn net.Listener httpListenAddr string handler http.Handler RootDeviceUUID string FriendlyName string // For waiting on the listener to close waitChan chan struct{} // Time interval between SSPD announces AnnounceInterval time.Duration } func New(ds model.DataStore, broker events.Broker) *DLNAServer { s := &DLNAServer{ ds: ds, broker: broker, ssdp: SSDPServer{ AnnounceInterval: time.Duration(30) * time.Second, Interfaces: listInterfaces(), FriendlyName: "Navidrome", RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), }, } s.ssdp.services = map[string]UPnPService{ "ContentDirectory": &contentDirectoryService{ DLNAServer: s, }, "ConnectionManager": &connectionManagerService{ DLNAServer: s, }, "X_MS_MediaReceiverRegistrar": &mediaReceiverRegistrarService{ DLNAServer: s, }, } //setup dedicated HTTP server for UPNP r := http.NewServeMux() r.Handle(resPath, http.StripPrefix(resPath, http.HandlerFunc(s.ssdp.resourceHandler))) r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) r.Handle("/static/", http.StripPrefix("/static/", withHeader("Cache-Control", "public, max-age=86400", http.FileServer(http.Dir("/tmp"))))) //TODO s.ssdp.handler = r return s } // Run starts the server with the given address, and if specified, with TLS enabled. func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) (err error) { if s.ssdp.HTTPConn == nil { network := "tcp4" if strings.Count(s.ssdp.httpListenAddr, ":") > 1 { network = "tcp" } s.ssdp.HTTPConn, err = net.Listen(network, s.ssdp.httpListenAddr) if err != nil { return } } go func() { s.ssdp.startSSDP() }() return nil } type UPnPService interface { Handle(action string, argsXML []byte, r *http.Request) (respArgs map[string]string, err error) Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error) Unsubscribe(sid string) error } func (s *SSDPServer) startSSDP() { active := 0 stopped := make(chan struct{}) for _, intf := range s.Interfaces { active++ go func(intf2 net.Interface) { defer func() { stopped <- struct{}{} }() s.ssdpInterface(intf2) }(intf) } for active > 0 { <-stopped active-- } } // Run SSDP server on an interface. func (s *SSDPServer) ssdpInterface(intf net.Interface) { // Figure out whether should an ip be announced ipfilterFn := func(ip net.IP) bool { listenaddr := s.HTTPConn.Addr().String() listenip := listenaddr[:strings.LastIndex(listenaddr, ":")] switch listenip { case "0.0.0.0": if strings.Contains(ip.String(), ":") { // Any IPv6 address should not be announced // because SSDP only listen on IPv4 multicast address return false } return true case "[::]": // In the @Serve() section, the default settings have been made to not listen on IPv6 addresses. // If actually still listening on [::], then allow to announce any address. return true default: if listenip == ip.String() { return true } return false } } // Figure out which HTTP location to advertise based on the interface IP. advertiseLocationFn := func(ip net.IP) string { url := url.URL{ Scheme: "http", Host: (&net.TCPAddr{ IP: ip, Port: s.HTTPConn.Addr().(*net.TCPAddr).Port, }).String(), Path: rootDescPath, } return url.String() } _, err := intf.Addrs() if err != nil { panic(err) } log.Printf("Started SSDP on %v", intf.Name) // Note that the devices and services advertised here via SSDP should be // in agreement with the rootDesc XML descriptor that is defined above. ssdpServer := ssdp.Server{ Interface: intf, Devices: []string{ "urn:schemas-upnp-org:device:MediaServer:1"}, Services: []string{ "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"}, IPFilter: ipfilterFn, Location: advertiseLocationFn, Server: serverField, UUID: s.RootDeviceUUID, NotifyInterval: s.AnnounceInterval, } // An interface with these flags should be valid for SSDP. const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast if err := ssdpServer.Init(); err != nil { if intf.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags { // Didn't expect it to work anyway. return } if strings.Contains(err.Error(), "listen") { // OSX has a lot of dud interfaces. Failure to create a socket on // the interface are what we're expecting if the interface is no // good. return } log.Printf("Error creating ssdp server on %s: %s", intf.Name, err) return } defer ssdpServer.Close() log.Printf("Started SSDP on %v", intf.Name) stopped := make(chan struct{}) go func() { defer close(stopped) if err := ssdpServer.Serve(); err != nil { log.Printf("%q: %q\n", intf.Name, err) } }() select { case <-s.waitChan: // Returning will close the server. case <-stopped: } } // Get all available active network interfaces. func listInterfaces() []net.Interface { ifs, err := net.Interfaces() if err != nil { log.Println("list network interfaces: %v", err) return []net.Interface{} } var active []net.Interface for _, intf := range ifs { if isAppropriatelyConfigured(intf) { active = append(active, intf) } } return active } func isAppropriatelyConfigured(intf net.Interface) bool { return intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0 } func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //remotePath := r.URL.Path w.Header().Set("Content-Length", strconv.FormatInt(1024, 10)) //TODO // add some DLNA specific headers if r.Header.Get("getContentFeatures.dlna.org") != "" { w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ SupportRange: true, }.String()) } w.Header().Set("transferMode.dlna.org", "Streaming") //http.ServeContent(w, r, remotePath, time.Now(), in) } func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { buffer := new(bytes.Buffer) w.Header().Set("content-type", `text/xml; charset="utf-8"`) w.Header().Set("cache-control", "private, max-age=60") w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10)) buffer.WriteTo(w) } // Handle a service control HTTP request. func (s *SSDPServer) serviceControlHandler(w http.ResponseWriter, r *http.Request) { soapActionString := r.Header.Get("SOAPACTION") soapAction, err := upnp.ParseActionHTTPHeader(soapActionString) if err != nil { serveError(s, w, "Could not parse SOAPACTION header", err) return } var env soap.Envelope if err := xml.NewDecoder(r.Body).Decode(&env); err != nil { serveError(s, w, "Could not parse SOAP request body", err) return } w.Header().Set("Content-Type", `text/xml; charset="utf-8"`) w.Header().Set("Ext", "") soapRespXML, code := func() ([]byte, int) { respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r) if err != nil { fmt.Printf("Error invoking %v: %v", soapAction, err) upnpErr := upnp.ConvertError(err) return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), http.StatusInternalServerError } return marshalSOAPResponse(soapAction, respArgs), http.StatusOK }() bodyStr := fmt.Sprintf(`%s`, soapRespXML) w.WriteHeader(code) if _, err := w.Write([]byte(bodyStr)); err != nil { fmt.Printf("Error writing response: %v", err) } } // Handle a SOAP request and return the response arguments or UPnP error. func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) (map[string]string, error) { service, ok := s.services[sa.Type] if !ok { // TODO: What's the invalid service error? return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type) } return service.Handle(sa.Action, actionRequestXML, r) } func didlLite(chardata string) string { return `` + chardata + `` } func mustMarshalXML(value interface{}) []byte { ret, err := xml.MarshalIndent(value, "", " ") if err != nil { log.Panicf("mustMarshalXML failed to marshal %v: %s", value, err) } return ret } // Marshal SOAP response arguments into a response XML snippet. func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte { soapArgs := make([]soap.Arg, 0, len(args)) for argName, value := range args { soapArgs = append(soapArgs, soap.Arg{ XMLName: xml.Name{Local: argName}, Value: value, }) } return []byte(fmt.Sprintf(`%[3]s`, sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs))) } func makeDeviceUUID(unique string) string { h := md5.New() if _, err := io.WriteString(h, unique); err != nil { log.Panicf("makeDeviceUUID write failed: %s", err) } buf := h.Sum(nil) return upnp.FormatUUID(buf) } // HTTP handler that sets headers. func withHeader(name string, value string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(name, value) next.ServeHTTP(w, r) }) } // serveError returns an http.StatusInternalServerError and logs the error func serveError(what interface{}, w http.ResponseWriter, text string, err error) { http.Error(w, text+".", http.StatusInternalServerError) }