aboutsummaryrefslogblamecommitdiffhomepage
path: root/server/tests/api/videos/video-static-file-privacy.ts
blob: 0a9864134f9b776b3c594ac2bbab4dc03f00ec64 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12



                                                                                              
                                                                                                         






                                                                                      

























                                                                
                                                       



                                                                    
                                                                                   



















                                                                                                                                
                                                      


























                                                                                                     
                                                                                                                        





                                                                                      
                                       
         








                                                          











                                                                                                                            
                                       











                                                                                                                    
                                    










                                                                                                                    
                                    





















                                                                                                
                                    
























                                                       
                            
        
                                                                                  








                                                                                                      





                                                                                      







                                                                                                     





                                                                                                  











                                                                                                                   
                                                                                                        




                                                                                                                          








































                                                                                                                                      























                                                                                                                           
















                                                                                                                                          







                                                                                                        
       

                                                                    
















                                                                                                                     


















                                                                                                                              




                                               

                                  

                                                                                                          



                                                                                                    
 












                                                                                                                                  









                                                                                                                                       
















































                                                                                                                                  




                                                               




                                 




                                                               


                                    










                                                               




                                                                                                
                                                                      




                                                                                                   






                                                                                                                            

      





























                                                                                                                      






















                                                                                                           
































                                                                                                         



                                  
/* 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/web-videos/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 ])
  })
})