f0e439de0d
Mostly trying to get logging to happen through rclone's log methods. Added request logging, and a trace parameter that will dump the entire request/response for debugging when dealing with poorly written clients. Also added a flag to specify the device's "Friendly Name" explicitly, and made an attempt at allowing mime types in addition to video.
455 lines
13 KiB
Go
455 lines
13 KiB
Go
package dlna
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"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/ncw/rclone/cmd"
|
|
"github.com/ncw/rclone/cmd/serve/dlna/data"
|
|
"github.com/ncw/rclone/cmd/serve/dlna/dlnaflags"
|
|
"github.com/ncw/rclone/fs"
|
|
"github.com/ncw/rclone/vfs"
|
|
"github.com/ncw/rclone/vfs/vfsflags"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
dlnaflags.AddFlags(Command.Flags())
|
|
vfsflags.AddFlags(Command.Flags())
|
|
}
|
|
|
|
// Command definition for cobra.
|
|
var Command = &cobra.Command{
|
|
Use: "dlna remote:path",
|
|
Short: `Serve remote:path over DLNA`,
|
|
Long: `rclone serve dlna is a DLNA media server for media stored in a rclone remote. Many
|
|
devices, such as the Xbox and PlayStation, can automatically discover this server in the LAN
|
|
and play audio/video from it. VLC is also supported. Service discovery uses UDP multicast
|
|
packets (SSDP) and will thus only work on LANs.
|
|
|
|
Rclone will list all files present in the remote, without filtering based on media formats or
|
|
file extensions. Additionally, there is no media transcoding support. This means that some
|
|
players might show files that they are not able to play back correctly.
|
|
|
|
` + dlnaflags.Help + vfs.Help,
|
|
Run: func(command *cobra.Command, args []string) {
|
|
cmd.CheckArgs(1, 1, command, args)
|
|
f := cmd.NewFsSrc(args)
|
|
|
|
cmd.Run(false, false, command, func() error {
|
|
s := newServer(f, &dlnaflags.Opt)
|
|
if err := s.Serve(); err != nil {
|
|
return err
|
|
}
|
|
s.Wait()
|
|
return nil
|
|
})
|
|
},
|
|
}
|
|
|
|
const (
|
|
serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0"
|
|
rootDescPath = "/rootDesc.xml"
|
|
resPath = "/res"
|
|
serviceControlURL = "/ctl"
|
|
)
|
|
|
|
type server 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
|
|
|
|
f fs.Fs
|
|
vfs *vfs.VFS
|
|
}
|
|
|
|
func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
|
|
friendlyName := opt.FriendlyName
|
|
if friendlyName == "" {
|
|
friendlyName = makeDefaultFriendlyName()
|
|
}
|
|
|
|
s := &server{
|
|
AnnounceInterval: 10 * time.Second,
|
|
FriendlyName: friendlyName,
|
|
RootDeviceUUID: makeDeviceUUID(friendlyName),
|
|
Interfaces: listInterfaces(),
|
|
|
|
httpListenAddr: opt.ListenAddr,
|
|
|
|
f: f,
|
|
vfs: vfs.New(f, &vfsflags.Opt),
|
|
}
|
|
|
|
s.services = map[string]UPnPService{
|
|
"ContentDirectory": &contentDirectoryService{
|
|
server: s,
|
|
},
|
|
"ConnectionManager": &connectionManagerService{
|
|
server: s,
|
|
},
|
|
}
|
|
|
|
// Setup the various http routes.
|
|
r := http.NewServeMux()
|
|
r.HandleFunc(resPath, s.resourceHandler)
|
|
if opt.LogTrace {
|
|
r.Handle(rootDescPath, traceLogging(http.HandlerFunc(s.rootDescHandler)))
|
|
r.Handle(serviceControlURL, traceLogging(http.HandlerFunc(s.serviceControlHandler)))
|
|
} else {
|
|
r.HandleFunc(rootDescPath, s.rootDescHandler)
|
|
r.HandleFunc(serviceControlURL, s.serviceControlHandler)
|
|
}
|
|
r.Handle("/static/", http.StripPrefix("/static/",
|
|
withHeader("Cache-Control", "public, max-age=86400",
|
|
http.FileServer(data.Assets))))
|
|
s.handler = logging(withHeader("Server", serverField, r))
|
|
|
|
return s
|
|
}
|
|
|
|
// UPnPService is the interface for the SOAP service.
|
|
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
|
|
}
|
|
|
|
// Formats the server as a string (used for logging.)
|
|
func (s *server) String() string {
|
|
return fmt.Sprintf("DLNA server on %v", s.httpListenAddr)
|
|
}
|
|
|
|
// Returns rclone version number as the model number.
|
|
func (s *server) ModelNumber() string {
|
|
return fs.Version
|
|
}
|
|
|
|
// Template used to generate the root device XML descriptor.
|
|
//
|
|
// Due to the use of namespaces and various subtleties with device compatibility,
|
|
// it turns out to be easier to use a template than to marshal XML.
|
|
//
|
|
// For rendering, it is passed the server object for context.
|
|
var rootDescTmpl = template.Must(template.New("rootDesc").Parse(`<?xml version="1.0"?>
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0"
|
|
xmlns:dlna="urn:schemas-dlna-org:device-1-0"
|
|
xmlns:sec="http://www.sec.co.kr/dlna">
|
|
<specVersion>
|
|
<major>1</major>
|
|
<minor>0</minor>
|
|
</specVersion>
|
|
<device>
|
|
<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
|
|
<friendlyName>{{.FriendlyName}}</friendlyName>
|
|
<manufacturer>rclone (rclone.org)</manufacturer>
|
|
<manufacturerURL>https://rclone.org/</manufacturerURL>
|
|
<modelDescription>rclone</modelDescription>
|
|
<modelName>rclone</modelName>
|
|
<modelNumber>{{.ModelNumber}}</modelNumber>
|
|
<modelURL>https://rclone.org/</modelURL>
|
|
<serialNumber>00000000</serialNumber>
|
|
<UDN>{{.RootDeviceUUID}}</UDN>
|
|
<dlna:X_DLNACAP/>
|
|
<dlna:X_DLNADOC>DMS-1.50</dlna:X_DLNADOC>
|
|
<dlna:X_DLNADOC>M-DMS-1.50</dlna:X_DLNADOC>
|
|
<sec:ProductCap>smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec</sec:ProductCap>
|
|
<sec:X_ProductCap>smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec</sec:X_ProductCap>
|
|
<iconList>
|
|
<icon>
|
|
<mimetype>image/png</mimetype>
|
|
<width>48</width>
|
|
<height>48</height>
|
|
<depth>8</depth>
|
|
<url>/static/rclone-48x48.png</url>
|
|
</icon>
|
|
<icon>
|
|
<mimetype>image/png</mimetype>
|
|
<width>120</width>
|
|
<height>120</height>
|
|
<depth>8</depth>
|
|
<url>/static/rclone-120x120.png</url>
|
|
</icon>
|
|
</iconList>
|
|
<serviceList>
|
|
<service>
|
|
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
|
|
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
|
|
<SCPDURL>/static/ContentDirectory.xml</SCPDURL>
|
|
<controlURL>/ctl</controlURL>
|
|
<eventSubURL></eventSubURL>
|
|
</service>
|
|
<service>
|
|
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
|
|
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
|
|
<SCPDURL>/static/ConnectionManager.xml</SCPDURL>
|
|
<controlURL>/ctl</controlURL>
|
|
<eventSubURL></eventSubURL>
|
|
</service>
|
|
<service>
|
|
<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>
|
|
<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>
|
|
<SCPDURL>/static/X_MS_MediaReceiverRegistrar.xml</SCPDURL>
|
|
<controlURL>/ctl</controlURL>
|
|
<eventSubURL></eventSubURL>
|
|
</service>
|
|
</serviceList>
|
|
<presentationURL>/</presentationURL>
|
|
</device>
|
|
</root>`))
|
|
|
|
// Renders the root device descriptor.
|
|
func (s *server) rootDescHandler(w http.ResponseWriter, r *http.Request) {
|
|
buffer := new(bytes.Buffer)
|
|
err := rootDescTmpl.Execute(buffer, s)
|
|
if err != nil {
|
|
serveError(s, w, "Failed to create root descriptor XML", err)
|
|
return
|
|
}
|
|
|
|
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))
|
|
_, err = buffer.WriteTo(w)
|
|
if err != nil {
|
|
// Network error
|
|
fs.Debugf(s, "Error writing rootDesc: %v", err)
|
|
}
|
|
}
|
|
|
|
// Handle a service control HTTP request.
|
|
func (s *server) 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 {
|
|
fs.Errorf(s, "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(`<?xml version="1.0" encoding="utf-8" standalone="yes"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>%s</s:Body></s:Envelope>`, soapRespXML)
|
|
w.WriteHeader(code)
|
|
if _, err := w.Write([]byte(bodyStr)); err != nil {
|
|
fs.Infof(s, "Error writing response: %v", err)
|
|
}
|
|
}
|
|
|
|
// Handle a SOAP request and return the response arguments or UPnP error.
|
|
func (s *server) 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)
|
|
}
|
|
|
|
// Serves actual resources (media files).
|
|
func (s *server) resourceHandler(w http.ResponseWriter, r *http.Request) {
|
|
remotePath := r.URL.Query().Get("path")
|
|
node, err := s.vfs.Stat(remotePath)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10))
|
|
|
|
// 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")
|
|
|
|
file := node.(*vfs.File)
|
|
in, err := file.Open(os.O_RDONLY)
|
|
if err != nil {
|
|
serveError(node, w, "Could not open resource", err)
|
|
return
|
|
}
|
|
defer fs.CheckClose(in, &err)
|
|
|
|
http.ServeContent(w, r, remotePath, node.ModTime(), in)
|
|
}
|
|
|
|
// Serve runs the server - returns the error only if
|
|
// the listener was not started; does not block, so
|
|
// use s.Wait() to block on the listener indefinitely.
|
|
func (s *server) Serve() (err error) {
|
|
if s.HTTPConn == nil {
|
|
s.HTTPConn, err = net.Listen("tcp", s.httpListenAddr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
s.startSSDP()
|
|
}()
|
|
|
|
go func() {
|
|
fs.Logf(s.f, "Serving HTTP on %s", s.HTTPConn.Addr().String())
|
|
|
|
err = s.serveHTTP()
|
|
if err != nil {
|
|
fs.Logf(s.f, "Error on serving HTTP server: %v", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Wait blocks while the listener is open.
|
|
func (s *server) Wait() {
|
|
<-s.waitChan
|
|
}
|
|
|
|
func (s *server) Close() {
|
|
err := s.HTTPConn.Close()
|
|
if err != nil {
|
|
fs.Errorf(s.f, "Error closing HTTP server: %v", err)
|
|
return
|
|
}
|
|
close(s.waitChan)
|
|
}
|
|
|
|
// Run SSDP (multicast for server discovery) on all interfaces.
|
|
func (s *server) 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 *server) ssdpInterface(intf net.Interface) {
|
|
// 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()
|
|
}
|
|
|
|
// 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"},
|
|
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
|
|
}
|
|
fs.Errorf(s, "Error creating ssdp server on %s: %s", intf.Name, err)
|
|
return
|
|
}
|
|
defer ssdpServer.Close()
|
|
fs.Infof(s, "Started SSDP on %v", intf.Name)
|
|
stopped := make(chan struct{})
|
|
go func() {
|
|
defer close(stopped)
|
|
if err := ssdpServer.Serve(); err != nil {
|
|
fs.Errorf(s, "%q: %q\n", intf.Name, err)
|
|
}
|
|
}()
|
|
select {
|
|
case <-s.waitChan:
|
|
// Returning will close the server.
|
|
case <-stopped:
|
|
}
|
|
}
|
|
|
|
func (s *server) serveHTTP() error {
|
|
srv := &http.Server{
|
|
Handler: s.handler,
|
|
}
|
|
err := srv.Serve(s.HTTPConn)
|
|
select {
|
|
case <-s.waitChan:
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|