diff --git a/cmd/serve/dlna/dlna.go b/cmd/serve/dlna/dlna.go index 02b37277f..962d64a71 100644 --- a/cmd/serve/dlna/dlna.go +++ b/cmd/serve/dlna/dlna.go @@ -116,6 +116,9 @@ func newServer(f fs.Fs, opt *dlnaflags.Options) *server { "ConnectionManager": &connectionManagerService{ server: s, }, + "X_MS_MediaReceiverRegistrar": &mediaReceiverRegistrarService{ + server: s, + }, } // Setup the various http routes. @@ -248,7 +251,7 @@ func (s *server) rootDescHandler(w http.ResponseWriter, r *http.Request) { // 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) + soapAction, err := parseActionHTTPHeader(soapActionString) if err != nil { serveError(s, w, "Could not parse SOAPACTION header", err) return diff --git a/cmd/serve/dlna/dlna_test.go b/cmd/serve/dlna/dlna_test.go index c83ded016..630058bc1 100644 --- a/cmd/serve/dlna/dlna_test.go +++ b/cmd/serve/dlna/dlna_test.go @@ -1,6 +1,7 @@ package dlna import ( + "bytes" "context" "fmt" "html" @@ -11,6 +12,8 @@ import ( "strings" "testing" + "github.com/anacrolix/dms/soap" + "github.com/rclone/rclone/vfs" _ "github.com/rclone/rclone/backend/local" @@ -125,3 +128,21 @@ func TestContentDirectoryBrowseMetadata(t *testing.T) { // with a non-zero childCount require.Contains(t, string(body), html.EscapeString(`childCount="1"`)) } + +// Check that the X_MS_MediaReceiverRegistrar is faked out properly. +func TestMediaReceiverRegistrarService(t *testing.T) { + env := soap.Envelope{ + Body: soap.Body{ + Action: []byte("RegisterDevice"), + }, + } + req, err := http.NewRequest("POST", testURL+"ctl", bytes.NewReader(mustMarshalXML(env))) + require.NoError(t, err) + req.Header.Set("SOAPACTION", `"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1#RegisterDevice"`) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "") +} diff --git a/cmd/serve/dlna/dlna_util.go b/cmd/serve/dlna/dlna_util.go index 45c6087cc..78b85aeaf 100644 --- a/cmd/serve/dlna/dlna_util.go +++ b/cmd/serve/dlna/dlna_util.go @@ -3,6 +3,7 @@ package dlna import ( "crypto/md5" "encoding/xml" + "errors" "fmt" "io" "log" @@ -11,6 +12,9 @@ import ( "net/http/httptest" "net/http/httputil" "os" + "regexp" + "strconv" + "strings" "github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/upnp" @@ -85,6 +89,36 @@ func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte { sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs))) } +var serviceURNRegexp = regexp.MustCompile(`:service:(\w+):(\d+)$`) + +func parseServiceType(s string) (ret upnp.ServiceURN, err error) { + matches := serviceURNRegexp.FindStringSubmatch(s) + if matches == nil { + err = errors.New(s) + return + } + if len(matches) != 3 { + log.Panicf("Invalid serviceURNRegexp ?") + } + ret.Type = matches[1] + ret.Version, err = strconv.ParseUint(matches[2], 0, 0) + return +} + +func parseActionHTTPHeader(s string) (ret upnp.SoapAction, err error) { + if s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + hashIndex := strings.LastIndex(s, "#") + if hashIndex == -1 { + return + } + ret.Action = s[hashIndex+1:] + ret.ServiceURN, err = parseServiceType(s[:hashIndex]) + return +} + type loggingResponseWriter struct { http.ResponseWriter request *http.Request diff --git a/cmd/serve/dlna/mrrs.go b/cmd/serve/dlna/mrrs.go new file mode 100644 index 000000000..70061bd71 --- /dev/null +++ b/cmd/serve/dlna/mrrs.go @@ -0,0 +1,27 @@ +package dlna + +import ( + "net/http" + + "github.com/anacrolix/dms/upnp" +) + +type mediaReceiverRegistrarService struct { + *server + upnp.Eventing +} + +func (mrrs *mediaReceiverRegistrarService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { + switch action { + case "IsAuthorized", "IsValidated": + return map[string]string{ + "Result": "1", + }, nil + case "RegisterDevice": + return map[string]string{ + "RegistrationRespMsg": mrrs.RootDeviceUUID, + }, nil + default: + return nil, upnp.InvalidActionError + } +}