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