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=