diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 63d14cf24..9e96c65d1 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -2,18 +2,60 @@ package dlna import ( "context" + "encoding/xml" + "fmt" + "log" + "net" + "net/http" + "net/url" + "strings" + "time" + "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} + s := &DLNAServer{ds: ds, broker: broker, ssdp: SSDPServer{}} + s.ssdp.Interfaces = listInterfaces() + return s } @@ -21,3 +63,173 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) error { 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 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))) +} diff --git a/go.mod b/go.mod index da005b7c7..383aaee53 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,9 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/atombender/go-jsonschema v0.20.0 // indirect + github.com/anacrolix/dms v1.7.1 // indirect + github.com/anacrolix/generics v0.0.1 // indirect + github.com/anacrolix/log v0.15.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/reflex v0.3.1 // indirect diff --git a/go.sum b/go.sum index 680bdf362..47f2211e3 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,12 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY= github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8= +github.com/anacrolix/dms v1.7.1 h1:XVOpT3eoO5Ds34B1X+TE3R2ApfqGGeqotEoCVNP8BaI= +github.com/anacrolix/dms v1.7.1/go.mod h1:excFJW5MKBhn5yt5ZMyeE9iFVqnO6tEGQl7YG/2tUoQ= +github.com/anacrolix/generics v0.0.1 h1:4WVhK6iLb3UAAAQP6I3uYlMOHcp9FqJC9j4n81Wv9Ks= +github.com/anacrolix/generics v0.0.1/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo= +github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=