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