mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Compiles, ssdp
This commit is contained in:
parent
56bdeef08a
commit
bdc146ab74
@ -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 `<DIDL-Lite` +
|
||||
` xmlns:dc="http://purl.org/dc/elements/1.1/"` +
|
||||
` xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"` +
|
||||
` xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"` +
|
||||
` xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">` +
|
||||
chardata +
|
||||
`</DIDL-Lite>`
|
||||
}
|
||||
|
||||
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(`<u:%[1]sResponse xmlns:u="%[2]s">%[3]s</u:%[1]sResponse>`,
|
||||
sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs)))
|
||||
}
|
||||
|
||||
3
go.mod
3
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user