]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
Merge branch 'release/4.3.0' into develop
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / p2p-media-loader / segment-validator.ts
1 import { basename } from 'path'
2 import { Segment } from '@peertube/p2p-media-loader-core'
3 import { logger } from '@root-helpers/logger'
4 import { wait } from '@root-helpers/utils'
5 import { isSameOrigin } from '../common'
6
7 type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } }
8
9 const maxRetries = 3
10
11 function segmentValidatorFactory (options: {
12 serverUrl: string
13 segmentsSha256Url: string
14 isLive: boolean
15 authorizationHeader: () => string
16 requiresAuth: boolean
17 }) {
18 const { serverUrl, segmentsSha256Url, isLive, authorizationHeader, requiresAuth } = options
19
20 let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
21 const regex = /bytes=(\d+)-(\d+)/
22
23 return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) {
24 // Wait for hash generation from the server
25 if (isLive) await wait(1000)
26
27 const filename = basename(segment.url)
28
29 const segmentValue = (await segmentsJSON)[filename]
30
31 if (!segmentValue && retry > maxRetries) {
32 throw new Error(`Unknown segment name ${filename} in segment validator`)
33 }
34
35 if (!segmentValue) {
36 logger.info(`Refetching sha segments for ${filename}`)
37
38 await wait(1000)
39
40 segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
41 await segmentValidator(segment, _method, _peerId, retry + 1)
42
43 return
44 }
45
46 let hashShouldBe: string
47 let range = ''
48
49 if (typeof segmentValue === 'string') {
50 hashShouldBe = segmentValue
51 } else {
52 const captured = regex.exec(segment.range)
53 range = captured[1] + '-' + captured[2]
54
55 hashShouldBe = segmentValue[range]
56 }
57
58 if (hashShouldBe === undefined) {
59 throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
60 }
61
62 const calculatedSha = await sha256Hex(segment.data)
63 if (calculatedSha !== hashShouldBe) {
64 throw new Error(
65 `Hashes does not correspond for segment ${filename}/${range}` +
66 `(expected: ${hashShouldBe} instead of ${calculatedSha})`
67 )
68 }
69 }
70 }
71
72 // ---------------------------------------------------------------------------
73
74 export {
75 segmentValidatorFactory
76 }
77
78 // ---------------------------------------------------------------------------
79
80 function fetchSha256Segments (options: {
81 serverUrl: string
82 segmentsSha256Url: string
83 authorizationHeader: () => string
84 requiresAuth: boolean
85 }) {
86 const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options
87
88 const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url)
89 ? { Authorization: authorizationHeader() }
90 : {}
91
92 return fetch(segmentsSha256Url, { headers })
93 .then(res => res.json() as Promise<SegmentsJSON>)
94 .catch(err => {
95 logger.error('Cannot get sha256 segments', err)
96 return {}
97 })
98 }
99
100 async function sha256Hex (data?: ArrayBuffer) {
101 if (!data) return undefined
102
103 if (window.crypto.subtle) {
104 return window.crypto.subtle.digest('SHA-256', data)
105 .then(data => bufferToHex(data))
106 }
107
108 // Fallback for non HTTPS context
109 const shaModule = (await import('sha.js') as any).default
110 // eslint-disable-next-line new-cap
111 return new shaModule.sha256().update(Buffer.from(data)).digest('hex')
112 }
113
114 // Thanks: https://stackoverflow.com/a/53307879
115 function bufferToHex (buffer?: ArrayBuffer) {
116 if (!buffer) return ''
117
118 let s = ''
119 const h = '0123456789abcdef'
120 const o = new Uint8Array(buffer)
121
122 o.forEach((v: any) => {
123 s += h[v >> 4] + h[v & 15]
124 })
125
126 return s
127 }