rclone/cmd/serve/dlna/dlna_util.go
Dan Walters f0e439de0d 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.
2019-05-27 14:42:33 +01:00

183 lines
4.8 KiB
Go

package dlna
import (
"crypto/md5"
"encoding/xml"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"github.com/anacrolix/dms/soap"
"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 {
h := md5.New()
if _, err := io.WriteString(h, unique); err != nil {
log.Panicf("makeDeviceUUID write failed: %s", err)
}
buf := h.Sum(nil)
return upnp.FormatUUID(buf)
}
// Get all available active network interfaces.
func listInterfaces() []net.Interface {
ifs, err := net.Interfaces()
if err != nil {
log.Printf("list network interfaces: %v", err)
return []net.Interface{}
}
var active []net.Interface
for _, intf := range ifs {
if intf.Flags&net.FlagUp == 0 || intf.MTU <= 0 {
continue
}
active = append(active, intf)
}
return active
}
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)))
}
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.
func withHeader(name string, value string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(name, value)
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)
}