/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import { expect } from 'chai' import { decode } from 'magnet-uri' import { checkVideoFileTokenReinjection, expectStartWith, parseTorrentVideo } from '@server/tests/shared' import { getAllFiles, wait } from '@shared/core-utils' import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' import { cleanupTests, createSingleServer, findExternalSavedVideo, makeRawRequest, PeerTubeServer, sendRTMPStream, setAccessTokensToServers, setDefaultVideoChannel, stopFfmpeg, waitJobs } from '@shared/server-commands' describe('Test video static file privacy', function () { let server: PeerTubeServer let userToken: string before(async function () { this.timeout(50000) server = await createSingleServer(1) await setAccessTokensToServers([ server ]) await setDefaultVideoChannel([ server ]) userToken = await server.users.generateUserAndToken('user1') }) describe('VOD static file path', function () { function runSuite () { async function checkPrivateFiles (uuid: string) { const video = await server.videos.getWithToken({ id: uuid }) for (const file of video.files) { expect(file.fileDownloadUrl).to.not.include('/private/') expectStartWith(file.fileUrl, server.url + '/static/webseed/private/') const torrent = await parseTorrentVideo(server, file) expect(torrent.urlList).to.have.lengthOf(0) const magnet = decode(file.magnetUri) expect(magnet.urlList).to.have.lengthOf(0) await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) } const hls = video.streamingPlaylists[0] if (hls) { expectStartWith(hls.playlistUrl, server.url + '/static/streaming-playlists/hls/private/') expectStartWith(hls.segmentsSha256Url, server.url + '/static/streaming-playlists/hls/private/') await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) } } async function checkPublicFiles (uuid: string) { const video = await server.videos.get({ id: uuid }) for (const file of getAllFiles(video)) { expect(file.fileDownloadUrl).to.not.include('/private/') expect(file.fileUrl).to.not.include('/private/') const torrent = await parseTorrentVideo(server, file) expect(torrent.urlList[0]).to.not.include('private') const magnet = decode(file.magnetUri) expect(magnet.urlList[0]).to.not.include('private') await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: torrent.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: magnet.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) } const hls = video.streamingPlaylists[0] if (hls) { expect(hls.playlistUrl).to.not.include('private') expect(hls.segmentsSha256Url).to.not.include('private') await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) } } it('Should upload a private/internal/password protected video and have a private static path', async function () { this.timeout(120000) for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { const { uuid } = await server.videos.quickUpload({ name: 'video', privacy }) await waitJobs([ server ]) await checkPrivateFiles(uuid) } const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'my super password' ] }) await waitJobs([ server ]) await checkPrivateFiles(uuid) }) it('Should upload a public video and update it as private/internal to have a private static path', async function () { this.timeout(120000) for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC }) await waitJobs([ server ]) await server.videos.update({ id: uuid, attributes: { privacy } }) await waitJobs([ server ]) await checkPrivateFiles(uuid) } }) it('Should upload a private video and update it to unlisted to have a public static path', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) await waitJobs([ server ]) await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } }) await waitJobs([ server ]) await checkPublicFiles(uuid) }) it('Should upload an internal video and update it to public to have a public static path', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) await waitJobs([ server ]) await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) await waitJobs([ server ]) await checkPublicFiles(uuid) }) it('Should upload an internal video and schedule a public publish', async function () { this.timeout(120000) const attributes = { name: 'video', privacy: VideoPrivacy.PRIVATE, scheduleUpdate: { updateAt: new Date(Date.now() + 1000).toISOString(), privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC } } const { uuid } = await server.videos.upload({ attributes }) await waitJobs([ server ]) await wait(1000) await server.debug.sendCommand({ body: { command: 'process-update-videos-scheduler' } }) await waitJobs([ server ]) await checkPublicFiles(uuid) }) } describe('Without transcoding', function () { runSuite() }) describe('With transcoding', function () { before(async function () { await server.config.enableMinimumTranscoding() }) runSuite() }) }) describe('VOD static file right check', function () { let unrelatedFileToken: string async function checkVideoFiles (options: { id: string expectedStatus: HttpStatusCode token: string videoFileToken: string videoPassword?: string }) { const { id, expectedStatus, token, videoFileToken, videoPassword } = options const video = await server.videos.getWithToken({ id }) for (const file of getAllFiles(video)) { await makeRawRequest({ url: file.fileUrl, token, expectedStatus }) await makeRawRequest({ url: file.fileDownloadUrl, token, expectedStatus }) await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) if (videoPassword) { const headers = { 'x-peertube-video-password': videoPassword } await makeRawRequest({ url: file.fileUrl, headers, expectedStatus }) await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus }) } } const hls = video.streamingPlaylists[0] await makeRawRequest({ url: hls.playlistUrl, token, expectedStatus }) await makeRawRequest({ url: hls.segmentsSha256Url, token, expectedStatus }) await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) if (videoPassword) { const headers = { 'x-peertube-video-password': videoPassword } await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus }) await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus }) } } before(async function () { await server.config.enableMinimumTranscoding() const { uuid } = await server.videos.quickUpload({ name: 'another video' }) unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) }) it('Should not be able to access a private video files without OAuth token and file token', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) }) it('Should not be able to access password protected video files without OAuth token, file token and password', async function () { this.timeout(120000) const videoPassword = 'my super password' const { uuid } = await server.videos.quickUpload({ name: 'password protected video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ videoPassword ] }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null, videoPassword: null }) }) it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () { this.timeout(120000) const videoPassword = 'my super password' const { uuid } = await server.videos.quickUpload({ name: 'password protected video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ videoPassword ] }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken, videoFileToken: unrelatedFileToken, videoPassword: 'incorrectPassword' }) }) it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken, videoFileToken: unrelatedFileToken }) }) it('Should be able to access a private video files with appropriate OAuth token or file token', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) }) it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () { this.timeout(120000) const videoPassword = 'my super password' const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ videoPassword ] }) const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword }) }) it('Should reinject video file token', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) await waitJobs([ server ]) { const video = await server.videos.getWithToken({ id: uuid }) const hls = video.streamingPlaylists[0] const query = { videoFileToken } const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) expect(text).to.not.include(videoFileToken) } { await checkVideoFileTokenReinjection({ server, videoUUID: uuid, videoFileToken, resolutions: [ 240, 720 ], isLive: false }) } }) it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () { this.timeout(120000) const { uuid } = await server.videos.quickUpload({ name: 'video', token: userToken, privacy: VideoPrivacy.PRIVATE }) const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) await waitJobs([ server ]) await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) }) }) describe('Live static file path and check', function () { let normalLiveId: string let normalLive: LiveVideo let permanentLiveId: string let permanentLive: LiveVideo let passwordProtectedLiveId: string let passwordProtectedLive: LiveVideo const correctPassword = 'my super password' let unrelatedFileToken: string async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) { const { live, liveId, videoPassword } = options const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) await server.live.waitUntilPublished({ videoId: liveId }) const video = await server.videos.getWithToken({ id: liveId }) const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) const hls = video.streamingPlaylists[0] for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) if (videoPassword) { await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url, headers: { 'x-peertube-video-password': 'incorrectPassword' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) } } await stopFfmpeg(ffmpegCommand) } async function checkReplay (replay: VideoDetails) { const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid }) const hls = replay.streamingPlaylists[0] expect(hls.files).to.not.have.lengthOf(0) for (const file of hls.files) { await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) } for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) } } before(async function () { await server.config.enableMinimumTranscoding() const { uuid } = await server.videos.quickUpload({ name: 'another video' }) unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) { const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) normalLiveId = video.uuid normalLive = live } { const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) permanentLiveId = video.uuid permanentLive = live } { const { video, live } = await server.live.quickCreate({ saveReplay: false, permanentLive: false, privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ correctPassword ] }) passwordProtectedLiveId = video.uuid passwordProtectedLive = live } }) it('Should create a private normal live and have a private static path', async function () { this.timeout(240000) await checkLiveFiles({ live: normalLive, liveId: normalLiveId }) }) it('Should create a private permanent live and have a private static path', async function () { this.timeout(240000) await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId }) }) it('Should create a password protected live and have a private static path', async function () { this.timeout(240000) await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword }) }) it('Should reinject video file token on permanent live', async function () { this.timeout(240000) const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey }) await server.live.waitUntilPublished({ videoId: permanentLiveId }) const video = await server.videos.getWithToken({ id: permanentLiveId }) const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) const hls = video.streamingPlaylists[0] { const query = { videoFileToken } const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) expect(text).to.not.include(videoFileToken) } { await checkVideoFileTokenReinjection({ server, videoUUID: permanentLiveId, videoFileToken, resolutions: [ 720 ], isLive: true }) } await stopFfmpeg(ffmpegCommand) }) it('Should have created a replay of the normal live with a private static path', async function () { this.timeout(240000) await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId }) const replay = await server.videos.getWithToken({ id: normalLiveId }) await checkReplay(replay) }) it('Should have created a replay of the permanent live with a private static path', async function () { this.timeout(240000) await server.live.waitUntilWaiting({ videoId: permanentLiveId }) await waitJobs([ server ]) const live = await server.videos.getWithToken({ id: permanentLiveId }) const replayFromList = await findExternalSavedVideo(server, live) const replay = await server.videos.getWithToken({ id: replayFromList.id }) await checkReplay(replay) }) }) describe('With static file right check disabled', function () { let videoUUID: string before(async function () { this.timeout(240000) await server.kill() await server.run({ static_files: { private_files_require_auth: false } }) const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) videoUUID = uuid await waitJobs([ server ]) }) it('Should not check auth for private static files', async function () { const video = await server.videos.getWithToken({ id: videoUUID }) for (const file of getAllFiles(video)) { await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) } const hls = video.streamingPlaylists[0] await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) }) }) after(async function () { await cleanupTests([ server ]) }) })