2019-01-03 23:23:50 +01:00
|
|
|
package dlna
|
|
|
|
|
|
|
|
import (
|
2019-09-14 21:03:48 +02:00
|
|
|
"bytes"
|
2019-06-17 10:34:30 +02:00
|
|
|
"context"
|
2019-01-03 23:23:50 +01:00
|
|
|
"fmt"
|
2019-09-15 19:33:45 +02:00
|
|
|
"html"
|
2022-08-20 16:38:02 +02:00
|
|
|
"io"
|
2019-01-03 23:23:50 +01:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2019-09-14 23:16:07 +02:00
|
|
|
"strings"
|
2019-01-03 23:23:50 +01:00
|
|
|
"testing"
|
|
|
|
|
2019-09-14 21:03:48 +02:00
|
|
|
"github.com/anacrolix/dms/soap"
|
|
|
|
|
2021-03-10 16:40:34 +01:00
|
|
|
"github.com/rclone/rclone/fs/config/configfile"
|
2019-07-28 19:47:38 +02:00
|
|
|
"github.com/rclone/rclone/vfs"
|
2019-01-03 23:23:50 +01:00
|
|
|
|
2019-07-28 19:47:38 +02:00
|
|
|
_ "github.com/rclone/rclone/backend/local"
|
|
|
|
"github.com/rclone/rclone/cmd/serve/dlna/dlnaflags"
|
|
|
|
"github.com/rclone/rclone/fs"
|
2019-01-03 23:23:50 +01:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
dlnaServer *server
|
2019-10-07 17:25:02 +02:00
|
|
|
baseURL string
|
2019-01-03 23:23:50 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2019-05-01 12:16:22 +02:00
|
|
|
testBindAddress = "localhost:0"
|
2019-01-03 23:23:50 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func startServer(t *testing.T, f fs.Fs) {
|
2024-07-03 19:14:48 +02:00
|
|
|
opt := dlnaflags.Opt
|
2019-01-03 23:23:50 +01:00
|
|
|
opt.ListenAddr = testBindAddress
|
2022-08-08 18:48:36 +02:00
|
|
|
var err error
|
|
|
|
dlnaServer, err = newServer(f, &opt)
|
|
|
|
assert.NoError(t, err)
|
2019-01-03 23:23:50 +01:00
|
|
|
assert.NoError(t, dlnaServer.Serve())
|
2019-10-07 17:25:02 +02:00
|
|
|
baseURL = "http://" + dlnaServer.HTTPConn.Addr().String()
|
2019-01-03 23:23:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestInit(t *testing.T) {
|
2021-04-26 23:37:49 +02:00
|
|
|
configfile.Install()
|
2019-01-03 23:23:50 +01:00
|
|
|
|
2020-11-05 16:18:51 +01:00
|
|
|
f, err := fs.NewFs(context.Background(), "testdata/files")
|
2019-06-17 10:34:30 +02:00
|
|
|
l, _ := f.List(context.Background(), "")
|
2019-01-03 23:23:50 +01:00
|
|
|
fmt.Println(l)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
startServer(t, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that it serves rootDesc.xml (SCPD in uPnP parlance).
|
|
|
|
func TestRootSCPD(t *testing.T) {
|
2019-10-07 17:25:02 +02:00
|
|
|
req, err := http.NewRequest("GET", baseURL+rootDescPath, nil)
|
2019-01-03 23:23:50 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2019-01-03 23:23:50 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
// Make sure that the SCPD contains a CDS service.
|
|
|
|
require.Contains(t, string(body),
|
|
|
|
"<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>")
|
2019-03-12 01:02:52 +01:00
|
|
|
// Make sure that the SCPD contains a CM service.
|
|
|
|
require.Contains(t, string(body),
|
|
|
|
"<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>")
|
2019-03-11 16:49:31 +01:00
|
|
|
// Ensure that the SCPD url is configured.
|
|
|
|
require.Regexp(t, "<SCPDURL>/.*</SCPDURL>", string(body))
|
2019-01-03 23:23:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that it serves content from the remote.
|
|
|
|
func TestServeContent(t *testing.T) {
|
2019-10-07 17:25:02 +02:00
|
|
|
req, err := http.NewRequest("GET", baseURL+resPath+"video.mp4", nil)
|
2019-01-03 23:23:50 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer fs.CheckClose(resp.Body, &err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
2022-08-20 16:38:02 +02:00
|
|
|
actualContents, err := io.ReadAll(resp.Body)
|
2019-01-03 23:23:50 +01:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Now compare the contents with the golden file.
|
2019-10-07 17:25:02 +02:00
|
|
|
node, err := dlnaServer.vfs.Stat("/video.mp4")
|
2019-01-03 23:23:50 +01:00
|
|
|
assert.NoError(t, err)
|
|
|
|
goldenFile := node.(*vfs.File)
|
|
|
|
goldenReader, err := goldenFile.Open(os.O_RDONLY)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
defer fs.CheckClose(goldenReader, &err)
|
2022-08-20 16:38:02 +02:00
|
|
|
goldenContents, err := io.ReadAll(goldenReader)
|
2019-01-03 23:23:50 +01:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
require.Equal(t, goldenContents, actualContents)
|
|
|
|
}
|
2019-09-14 23:16:07 +02:00
|
|
|
|
|
|
|
// Check that ContentDirectory#Browse returns appropriate metadata on the root container.
|
|
|
|
func TestContentDirectoryBrowseMetadata(t *testing.T) {
|
|
|
|
// Sample from: https://github.com/rclone/rclone/issues/3253#issuecomment-524317469
|
2019-10-07 17:25:02 +02:00
|
|
|
req, err := http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
|
2019-09-14 23:16:07 +02:00
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
|
|
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
|
|
|
<ObjectID>0</ObjectID>
|
|
|
|
<BrowseFlag>BrowseMetadata</BrowseFlag>
|
|
|
|
<Filter>*</Filter>
|
|
|
|
<StartingIndex>0</StartingIndex>
|
|
|
|
<RequestedCount>0</RequestedCount>
|
|
|
|
<SortCriteria></SortCriteria>
|
|
|
|
</u:Browse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>`))
|
|
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2019-09-14 23:16:07 +02:00
|
|
|
require.NoError(t, err)
|
2022-08-06 02:59:00 +02:00
|
|
|
// should contain an appropriate URN
|
|
|
|
require.Contains(t, string(body), "urn:schemas-upnp-org:service:ContentDirectory:1")
|
2019-09-15 19:33:45 +02:00
|
|
|
// expect a <container> element
|
|
|
|
require.Contains(t, string(body), html.EscapeString("<container "))
|
|
|
|
require.NotContains(t, string(body), html.EscapeString("<item "))
|
2020-02-18 01:13:17 +01:00
|
|
|
// if there is a childCount, it better not be zero
|
|
|
|
require.NotContains(t, string(body), html.EscapeString(" childCount=\"0\""))
|
2020-02-18 01:49:35 +01:00
|
|
|
// should have a dc:date element
|
|
|
|
require.Contains(t, string(body), html.EscapeString("<dc:date>"))
|
2019-09-14 23:16:07 +02:00
|
|
|
}
|
2019-09-14 21:03:48 +02:00
|
|
|
|
|
|
|
// Check that the X_MS_MediaReceiverRegistrar is faked out properly.
|
|
|
|
func TestMediaReceiverRegistrarService(t *testing.T) {
|
|
|
|
env := soap.Envelope{
|
|
|
|
Body: soap.Body{
|
|
|
|
Action: []byte("RegisterDevice"),
|
|
|
|
},
|
|
|
|
}
|
2019-10-07 17:25:02 +02:00
|
|
|
req, err := http.NewRequest("POST", baseURL+serviceControlURL, bytes.NewReader(mustMarshalXML(env)))
|
2019-09-14 21:03:48 +02:00
|
|
|
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)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2019-09-14 21:03:48 +02:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, string(body), "<RegistrationRespMsg>")
|
|
|
|
}
|
2019-10-07 17:25:02 +02:00
|
|
|
|
|
|
|
// Check that ContentDirectory#Browse returns the expected items.
|
|
|
|
func TestContentDirectoryBrowseDirectChildren(t *testing.T) {
|
|
|
|
// First the root...
|
|
|
|
req, err := http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
|
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
|
|
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
|
|
|
<ObjectID>0</ObjectID>
|
|
|
|
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
|
|
|
|
<Filter>*</Filter>
|
|
|
|
<StartingIndex>0</StartingIndex>
|
|
|
|
<RequestedCount>0</RequestedCount>
|
|
|
|
<SortCriteria></SortCriteria>
|
|
|
|
</u:Browse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>`))
|
|
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2019-10-07 17:25:02 +02:00
|
|
|
require.NoError(t, err)
|
|
|
|
// expect video.mp4, video.srt, video.en.srt URLs to be in the DIDL
|
|
|
|
require.Contains(t, string(body), "/r/video.mp4")
|
|
|
|
require.Contains(t, string(body), "/r/video.srt")
|
|
|
|
require.Contains(t, string(body), "/r/video.en.srt")
|
|
|
|
|
2023-11-24 14:23:14 +01:00
|
|
|
// Then a subdirectory (subdir)
|
|
|
|
{
|
|
|
|
req, err = http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
|
2019-10-07 17:25:02 +02:00
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
|
|
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
|
|
|
<ObjectID>%2Fsubdir</ObjectID>
|
|
|
|
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
|
|
|
|
<Filter>*</Filter>
|
|
|
|
<StartingIndex>0</StartingIndex>
|
|
|
|
<RequestedCount>0</RequestedCount>
|
|
|
|
<SortCriteria></SortCriteria>
|
|
|
|
</u:Browse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>`))
|
2023-11-24 14:23:14 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, err = io.ReadAll(resp.Body)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// expect video.mp4, video.srt, URLs to be in the DIDL
|
|
|
|
require.Contains(t, string(body), "/r/subdir/video.mp4")
|
|
|
|
require.Contains(t, string(body), "/r/subdir/video.srt")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then a subdirectory with subtitles separately (subdir2)
|
|
|
|
{
|
|
|
|
req, err = http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
|
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
|
|
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
|
|
|
<ObjectID>%2Fsubdir2</ObjectID>
|
|
|
|
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
|
|
|
|
<Filter>*</Filter>
|
|
|
|
<StartingIndex>0</StartingIndex>
|
|
|
|
<RequestedCount>0</RequestedCount>
|
|
|
|
<SortCriteria></SortCriteria>
|
|
|
|
</u:Browse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>`))
|
|
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, err = io.ReadAll(resp.Body)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// expect video.mp4, Subs/video.srt, URLs to be in the DIDL
|
|
|
|
require.Contains(t, string(body), "/r/subdir2/video.mp4")
|
|
|
|
require.Contains(t, string(body), "/r/subdir2/Subs/video.srt")
|
|
|
|
|
|
|
|
}
|
2019-10-07 17:25:02 +02:00
|
|
|
}
|