dlna: improve logging and error handling

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.
This commit is contained in:
Dan Walters 2019-05-26 13:51:22 -05:00 committed by Nick Craig-Wood
parent e5464a2a35
commit f0e439de0d
4 changed files with 171 additions and 37 deletions

View file

@ -9,11 +9,13 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/dlna"
"github.com/anacrolix/dms/upnp" "github.com/anacrolix/dms/upnp"
"github.com/anacrolix/dms/upnpav" "github.com/anacrolix/dms/upnpav"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/vfs" "github.com/ncw/rclone/vfs"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -27,6 +29,8 @@ func (cds *contentDirectoryService) updateIDString() string {
return fmt.Sprintf("%d", uint32(os.Getpid())) return fmt.Sprintf("%d", uint32(os.Getpid()))
} }
var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/")
// Turns the given entry and DMS host into a UPnP object. A nil object is // Turns the given entry and DMS host into a UPnP object. A nil object is
// returned if the entry is not of interest. // returned if the entry is not of interest.
func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host string) (ret interface{}, err error) { func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host string) (ret interface{}, err error) {
@ -47,8 +51,13 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi
return return
} }
// Hardcode "videoItem" so that files show up in VLC. mimeType := fs.MimeTypeFromName(fileInfo.Name())
obj.Class = "object.item.videoItem" mediaType := mediaMimeTypeRegexp.FindStringSubmatch(mimeType)
if mediaType == nil {
return
}
obj.Class = "object.item." + mediaType[1] + "Item"
obj.Title = fileInfo.Name() obj.Title = fileInfo.Name()
item := upnpav.Item{ item := upnpav.Item{
@ -65,8 +74,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi
"path": {cdsObject.Path}, "path": {cdsObject.Path},
}.Encode(), }.Encode(),
}).String(), }).String(),
// Hardcode "video/x-matroska" so that files show up in VLC. ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{
ProtocolInfo: fmt.Sprintf("http-get:*:video/x-matroska:%s", dlna.ContentFeatures{
SupportRange: true, SupportRange: true,
}.String()), }.String()),
Bitrate: 0, Bitrate: 0,
@ -106,14 +114,14 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
} }
obj, err := cds.cdsObjectToUpnpavObject(child, de, host) obj, err := cds.cdsObjectToUpnpavObject(child, de, host)
if err != nil { if err != nil {
log.Printf("error with %s: %s", child.FilePath(), err) fs.Errorf(cds, "error with %s: %s", child.FilePath(), err)
continue continue
} }
if obj != nil { if obj == nil {
ret = append(ret, obj) fs.Debugf(cds, "unrecognized file type: %s", de)
} else { continue
log.Printf("bad %s", de)
} }
ret = append(ret, obj)
} }
return return

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -53,7 +52,7 @@ players might show files that they are not able to play back correctly.
cmd.Run(false, false, command, func() error { cmd.Run(false, false, command, func() error {
s := newServer(f, &dlnaflags.Opt) s := newServer(f, &dlnaflags.Opt)
if err := s.Serve(); err != nil { if err := s.Serve(); err != nil {
log.Fatal(err) return err
} }
s.Wait() s.Wait()
return nil return nil
@ -93,23 +92,23 @@ type server struct {
} }
func newServer(f fs.Fs, opt *dlnaflags.Options) *server { func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
hostName, err := os.Hostname() friendlyName := opt.FriendlyName
if err != nil { if friendlyName == "" {
hostName = "" friendlyName = makeDefaultFriendlyName()
} else {
hostName = " (" + hostName + ")"
} }
s := &server{ s := &server{
AnnounceInterval: 10 * time.Second, AnnounceInterval: 10 * time.Second,
FriendlyName: friendlyName,
RootDeviceUUID: makeDeviceUUID(friendlyName),
Interfaces: listInterfaces(), Interfaces: listInterfaces(),
FriendlyName: "rclone" + hostName,
httpListenAddr: opt.ListenAddr, httpListenAddr: opt.ListenAddr,
f: f, f: f,
vfs: vfs.New(f, &vfsflags.Opt), vfs: vfs.New(f, &vfsflags.Opt),
} }
s.services = map[string]UPnPService{ s.services = map[string]UPnPService{
"ContentDirectory": &contentDirectoryService{ "ContentDirectory": &contentDirectoryService{
server: s, server: s,
@ -118,17 +117,21 @@ func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
server: s, server: s,
}, },
} }
s.RootDeviceUUID = makeDeviceUUID(s.FriendlyName)
// Setup the various http routes. // Setup the various http routes.
r := http.NewServeMux() r := http.NewServeMux()
r.HandleFunc(resPath, s.resourceHandler) r.HandleFunc(resPath, s.resourceHandler)
r.HandleFunc(rootDescPath, s.rootDescHandler) if opt.LogTrace {
r.HandleFunc(serviceControlURL, s.serviceControlHandler) 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/", r.Handle("/static/", http.StripPrefix("/static/",
withHeader("Cache-Control", "public, max-age=86400", withHeader("Cache-Control", "public, max-age=86400",
http.FileServer(data.Assets)))) http.FileServer(data.Assets))))
s.handler = withHeader("Server", serverField, r) s.handler = logging(withHeader("Server", serverField, r))
return s return s
} }
@ -140,6 +143,11 @@ type UPnPService interface {
Unsubscribe(sid string) 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. // Returns rclone version number as the model number.
func (s *server) ModelNumber() string { func (s *server) ModelNumber() string {
return fs.Version return fs.Version
@ -223,14 +231,18 @@ func (s *server) rootDescHandler(w http.ResponseWriter, r *http.Request) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
err := rootDescTmpl.Execute(buffer, s) err := rootDescTmpl.Execute(buffer, s)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) serveError(s, w, "Failed to create root descriptor XML", err)
return return
} }
w.Header().Set("content-type", `text/xml; charset="utf-8"`) w.Header().Set("content-type", `text/xml; charset="utf-8"`)
w.Header().Set("cache-control", "private, max-age=60") w.Header().Set("cache-control", "private, max-age=60")
w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10)) w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10))
buffer.WriteTo(w) _, err = buffer.WriteTo(w)
if err != nil {
// Network error
fs.Debugf(s, "Error writing rootDesc: %v", err)
}
} }
// Handle a service control HTTP request. // Handle a service control HTTP request.
@ -238,30 +250,30 @@ func (s *server) serviceControlHandler(w http.ResponseWriter, r *http.Request) {
soapActionString := r.Header.Get("SOAPACTION") soapActionString := r.Header.Get("SOAPACTION")
soapAction, err := upnp.ParseActionHTTPHeader(soapActionString) soapAction, err := upnp.ParseActionHTTPHeader(soapActionString)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) serveError(s, w, "Could not parse SOAPACTION header", err)
return return
} }
var env soap.Envelope var env soap.Envelope
if err := xml.NewDecoder(r.Body).Decode(&env); err != nil { if err := xml.NewDecoder(r.Body).Decode(&env); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) serveError(s, w, "Could not parse SOAP request body", err)
return return
} }
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`) w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
w.Header().Set("Ext", "") w.Header().Set("Ext", "")
w.Header().Set("server", serverField)
soapRespXML, code := func() ([]byte, int) { soapRespXML, code := func() ([]byte, int) {
respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r) respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r)
if err != nil { if err != nil {
fs.Errorf(s, "Error invoking %v: %v", soapAction, err)
upnpErr := upnp.ConvertError(err) upnpErr := upnp.ConvertError(err)
return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), 500 return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), http.StatusInternalServerError
} }
return marshalSOAPResponse(soapAction, respArgs), 200 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) 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) w.WriteHeader(code)
if _, err := w.Write([]byte(bodyStr)); err != nil { if _, err := w.Write([]byte(bodyStr)); err != nil {
log.Print(err) fs.Infof(s, "Error writing response: %v", err)
} }
} }
@ -280,7 +292,7 @@ func (s *server) resourceHandler(w http.ResponseWriter, r *http.Request) {
remotePath := r.URL.Query().Get("path") remotePath := r.URL.Query().Get("path")
node, err := s.vfs.Stat(remotePath) node, err := s.vfs.Stat(remotePath)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.NotFound(w, r)
return return
} }
@ -297,12 +309,12 @@ func (s *server) resourceHandler(w http.ResponseWriter, r *http.Request) {
file := node.(*vfs.File) file := node.(*vfs.File)
in, err := file.Open(os.O_RDONLY) in, err := file.Open(os.O_RDONLY)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) serveError(node, w, "Could not open resource", err)
return
} }
defer fs.CheckClose(in, &err) defer fs.CheckClose(in, &err)
http.ServeContent(w, r, remotePath, node.ModTime(), in) http.ServeContent(w, r, remotePath, node.ModTime(), in)
return
} }
// Serve runs the server - returns the error only if // Serve runs the server - returns the error only if
@ -410,16 +422,16 @@ func (s *server) ssdpInterface(intf net.Interface) {
// good. // good.
return return
} }
log.Printf("Error creating ssdp server on %s: %s", intf.Name, err) fs.Errorf(s, "Error creating ssdp server on %s: %s", intf.Name, err)
return return
} }
defer ssdpServer.Close() defer ssdpServer.Close()
log.Println("Started SSDP on", intf.Name) fs.Infof(s, "Started SSDP on %v", intf.Name)
stopped := make(chan struct{}) stopped := make(chan struct{})
go func() { go func() {
defer close(stopped) defer close(stopped)
if err := ssdpServer.Serve(); err != nil { if err := ssdpServer.Serve(); err != nil {
log.Printf("%q: %q\n", intf.Name, err) fs.Errorf(s, "%q: %q\n", intf.Name, err)
} }
}() }()
select { select {

View file

@ -8,11 +8,26 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/soap"
"github.com/anacrolix/dms/upnp" "github.com/anacrolix/dms/upnp"
"github.com/ncw/rclone/fs"
) )
// Return a default "friendly name" for the server.
func makeDefaultFriendlyName() string {
hostName, err := os.Hostname()
if err != nil {
hostName = ""
} else {
hostName = " (" + hostName + ")"
}
return "rclone" + hostName
}
func makeDeviceUUID(unique string) string { func makeDeviceUUID(unique string) string {
h := md5.New() h := md5.New()
if _, err := io.WriteString(h, unique); err != nil { if _, err := io.WriteString(h, unique); err != nil {
@ -71,6 +86,87 @@ func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte {
sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs))) sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs)))
} }
type loggingResponseWriter struct {
http.ResponseWriter
request *http.Request
committed bool
}
func (lrw *loggingResponseWriter) logRequest(code int, err interface{}) {
// Choose appropriate log level based on response status code.
var level fs.LogLevel
if code < 400 && err == nil {
level = fs.LogLevelInfo
} else {
level = fs.LogLevelError
}
fs.LogPrintf(level, lrw.request.URL.Path, "%s %s %d %s %s",
lrw.request.RemoteAddr, lrw.request.Method, code,
lrw.request.Header.Get("SOAPACTION"), err)
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.committed = true
lrw.logRequest(code, nil)
lrw.ResponseWriter.WriteHeader(code)
}
// HTTP handler that logs requests and any errors or panics.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lrw := &loggingResponseWriter{ResponseWriter: w, request: r}
defer func() {
err := recover()
if err != nil {
if !lrw.committed {
lrw.logRequest(http.StatusInternalServerError, err)
http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
} else {
// Too late to send the error to client, but at least log it.
fs.Errorf(r.URL.Path, "Recovered panic: %v", err)
}
}
}()
next.ServeHTTP(lrw, r)
})
}
// HTTP handler that logs complete request and response bodies for debugging.
// Error recovery and general request logging are left to logging().
func traceLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dump, err := httputil.DumpRequest(r, true)
if err != nil {
serveError(nil, w, "error dumping request", err)
return
}
fs.Debugf(nil, "%s", dump)
recorder := httptest.NewRecorder()
next.ServeHTTP(recorder, r)
dump, err = httputil.DumpResponse(recorder.Result(), true)
if err != nil {
// log the error but ignore it
fs.Errorf(nil, "error dumping response: %v", err)
} else {
fs.Debugf(nil, "%s", dump)
}
// copy from recorder to the real response writer
for k, v := range recorder.Header() {
w.Header()[k] = v
}
w.WriteHeader(recorder.Code)
_, err = recorder.Body.WriteTo(w)
if err != nil {
// Network error
fs.Debugf(nil, "Error writing response: %v", err)
}
})
}
// HTTP handler that sets headers. // HTTP handler that sets headers.
func withHeader(name string, value string, next http.Handler) http.Handler { func withHeader(name string, value string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -78,3 +174,10 @@ func withHeader(name string, value string, next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
// serveError returns an http.StatusInternalServerError and logs the error
func serveError(what interface{}, w http.ResponseWriter, text string, err error) {
fs.CountError(err)
fs.Errorf(what, "%s: %v", text, err)
http.Error(w, text+".", http.StatusInternalServerError)
}

View file

@ -14,16 +14,25 @@ Use --addr to specify which IP address and port the server should
listen on, eg --addr 1.2.3.4:8000 or --addr :8080 to listen to all listen on, eg --addr 1.2.3.4:8000 or --addr :8080 to listen to all
IPs. IPs.
Use --name to choose the friendly server name, which is by
default "rclone (hostname)".
Use --log-trace in conjunction with -vv to enable additional debug
logging of all UPNP traffic.
` `
// Options is the type for DLNA serving options. // Options is the type for DLNA serving options.
type Options struct { type Options struct {
ListenAddr string ListenAddr string
FriendlyName string
LogTrace bool
} }
// DefaultOpt contains the defaults options for DLNA serving. // DefaultOpt contains the defaults options for DLNA serving.
var DefaultOpt = Options{ var DefaultOpt = Options{
ListenAddr: ":7879", ListenAddr: ":7879",
FriendlyName: "",
LogTrace: false,
} }
// Opt contains the options for DLNA serving. // Opt contains the options for DLNA serving.
@ -34,6 +43,8 @@ var (
func addFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *Options) { func addFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *Options) {
rc.AddOption("dlna", &Opt) rc.AddOption("dlna", &Opt)
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "ip:port or :port to bind the DLNA http server to.") flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "ip:port or :port to bind the DLNA http server to.")
flags.StringVarP(flagSet, &Opt.FriendlyName, prefix+"name", "", Opt.FriendlyName, "name of DLNA server")
flags.BoolVarP(flagSet, &Opt.LogTrace, prefix+"log-trace", "", Opt.LogTrace, "enable trace logging of SOAP traffic")
} }
// AddFlags add the command line flags for DLNA serving. // AddFlags add the command line flags for DLNA serving.