]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / helpers / core-utils.ts
1 /* eslint-disable no-useless-call */
2
3 /*
4 Different from 'utils' because we don't import other PeerTube modules.
5 Useful to avoid circular dependencies.
6 */
7
8 import { exec, ExecOptions } from 'child_process'
9 import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto'
10 import { truncate } from 'lodash'
11 import { pipeline } from 'stream'
12 import { URL } from 'url'
13 import { promisify } from 'util'
14 import { promisify1, promisify2, promisify3 } from '@shared/core-utils'
15
16 const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
17 if (!oldObject || typeof oldObject !== 'object') {
18 return valueConverter(oldObject)
19 }
20
21 if (Array.isArray(oldObject)) {
22 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
23 }
24
25 const newObject = {}
26 Object.keys(oldObject).forEach(oldKey => {
27 const newKey = keyConverter(oldKey)
28 newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
29 })
30
31 return newObject
32 }
33
34 function mapToJSON (map: Map<any, any>) {
35 const obj: any = {}
36
37 for (const [ k, v ] of map) {
38 obj[k] = v
39 }
40
41 return obj
42 }
43
44 // ---------------------------------------------------------------------------
45
46 const timeTable = {
47 ms: 1,
48 second: 1000,
49 minute: 60000,
50 hour: 3600000,
51 day: 3600000 * 24,
52 week: 3600000 * 24 * 7,
53 month: 3600000 * 24 * 30
54 }
55
56 export function parseDurationToMs (duration: number | string): number {
57 if (duration === null) return null
58 if (typeof duration === 'number') return duration
59 if (!isNaN(+duration)) return +duration
60
61 if (typeof duration === 'string') {
62 const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
63
64 if (split.length === 3) {
65 const len = parseFloat(split[1])
66 let unit = split[2].replace(/s$/i, '').toLowerCase()
67 if (unit === 'm') {
68 unit = 'ms'
69 }
70
71 return (len || 1) * (timeTable[unit] || 0)
72 }
73 }
74
75 throw new Error(`Duration ${duration} could not be properly parsed`)
76 }
77
78 export function parseBytes (value: string | number): number {
79 if (typeof value === 'number') return value
80 if (!isNaN(+value)) return +value
81
82 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
83 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
84 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
85 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
86 const t = /^(\d+)\s*TB$/
87 const g = /^(\d+)\s*GB$/
88 const m = /^(\d+)\s*MB$/
89 const b = /^(\d+)\s*B$/
90
91 let match: RegExpMatchArray
92
93 if (value.match(tgm)) {
94 match = value.match(tgm)
95 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
96 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
97 parseInt(match[3], 10) * 1024 * 1024
98 }
99
100 if (value.match(tg)) {
101 match = value.match(tg)
102 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
103 parseInt(match[2], 10) * 1024 * 1024 * 1024
104 }
105
106 if (value.match(tm)) {
107 match = value.match(tm)
108 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
109 parseInt(match[2], 10) * 1024 * 1024
110 }
111
112 if (value.match(gm)) {
113 match = value.match(gm)
114 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
115 parseInt(match[2], 10) * 1024 * 1024
116 }
117
118 if (value.match(t)) {
119 match = value.match(t)
120 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
121 }
122
123 if (value.match(g)) {
124 match = value.match(g)
125 return parseInt(match[1], 10) * 1024 * 1024 * 1024
126 }
127
128 if (value.match(m)) {
129 match = value.match(m)
130 return parseInt(match[1], 10) * 1024 * 1024
131 }
132
133 if (value.match(b)) {
134 match = value.match(b)
135 return parseInt(match[1], 10) * 1024
136 }
137
138 return parseInt(value, 10)
139 }
140
141 // ---------------------------------------------------------------------------
142
143 function sanitizeUrl (url: string) {
144 const urlObject = new URL(url)
145
146 if (urlObject.protocol === 'https:' && urlObject.port === '443') {
147 urlObject.port = ''
148 } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
149 urlObject.port = ''
150 }
151
152 return urlObject.href.replace(/\/$/, '')
153 }
154
155 // Don't import remote scheme from constants because we are in core utils
156 function sanitizeHost (host: string, remoteScheme: string) {
157 const toRemove = remoteScheme === 'https' ? 443 : 80
158
159 return host.replace(new RegExp(`:${toRemove}$`), '')
160 }
161
162 // ---------------------------------------------------------------------------
163
164 function isTestInstance () {
165 return process.env.NODE_ENV === 'test'
166 }
167
168 function isDevInstance () {
169 return process.env.NODE_ENV === 'dev'
170 }
171
172 function isTestOrDevInstance () {
173 return isTestInstance() || isDevInstance()
174 }
175
176 function isProdInstance () {
177 return process.env.NODE_ENV === 'production'
178 }
179
180 function getAppNumber () {
181 return process.env.NODE_APP_INSTANCE || ''
182 }
183
184 // ---------------------------------------------------------------------------
185
186 // Consistent with .length, lodash truncate function is not
187 function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
188 const truncatedStr = truncate(str, options)
189
190 // The truncated string is okay, we can return it
191 if (truncatedStr.length <= options.length) return truncatedStr
192
193 // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
194 // We always use the .length so we need to truncate more if needed
195 options.length -= truncatedStr.length - options.length
196 return truncate(str, options)
197 }
198
199 function pageToStartAndCount (page: number, itemsPerPage: number) {
200 const start = (page - 1) * itemsPerPage
201
202 return { start, count: itemsPerPage }
203 }
204
205 // ---------------------------------------------------------------------------
206
207 type SemVersion = { major: number, minor: number, patch: number }
208 function parseSemVersion (s: string) {
209 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
210
211 return {
212 major: parseInt(parsed[1]),
213 minor: parseInt(parsed[2]),
214 patch: parseInt(parsed[3])
215 } as SemVersion
216 }
217
218 // ---------------------------------------------------------------------------
219
220 function execShell (command: string, options?: ExecOptions) {
221 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
222 exec(command, options, (err, stdout, stderr) => {
223 // eslint-disable-next-line prefer-promise-reject-errors
224 if (err) return rej({ err, stdout, stderr })
225
226 return res({ stdout, stderr })
227 })
228 })
229 }
230
231 // ---------------------------------------------------------------------------
232
233 function generateRSAKeyPairPromise (size: number) {
234 return new Promise<{ publicKey: string, privateKey: string }>((res, rej) => {
235 const options: RSAKeyPairOptions<'pem', 'pem'> = {
236 modulusLength: size,
237 publicKeyEncoding: {
238 type: 'spki',
239 format: 'pem'
240 },
241 privateKeyEncoding: {
242 type: 'pkcs1',
243 format: 'pem'
244 }
245 }
246
247 generateKeyPair('rsa', options, (err, publicKey, privateKey) => {
248 if (err) return rej(err)
249
250 return res({ publicKey, privateKey })
251 })
252 })
253 }
254
255 function generateED25519KeyPairPromise () {
256 return new Promise<{ publicKey: string, privateKey: string }>((res, rej) => {
257 const options: ED25519KeyPairOptions<'pem', 'pem'> = {
258 publicKeyEncoding: {
259 type: 'spki',
260 format: 'pem'
261 },
262 privateKeyEncoding: {
263 type: 'pkcs8',
264 format: 'pem'
265 }
266 }
267
268 generateKeyPair('ed25519', options, (err, publicKey, privateKey) => {
269 if (err) return rej(err)
270
271 return res({ publicKey, privateKey })
272 })
273 })
274 }
275
276 // ---------------------------------------------------------------------------
277
278 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
279 const scryptPromise = promisify3<string, string, number, Buffer>(scrypt)
280 const execPromise2 = promisify2<string, any, string>(exec)
281 const execPromise = promisify1<string, string>(exec)
282 const pipelinePromise = promisify(pipeline)
283
284 // ---------------------------------------------------------------------------
285
286 export {
287 isTestInstance,
288 isTestOrDevInstance,
289 isProdInstance,
290 getAppNumber,
291
292 objectConverter,
293 mapToJSON,
294
295 sanitizeUrl,
296 sanitizeHost,
297
298 execShell,
299
300 pageToStartAndCount,
301 peertubeTruncate,
302
303 scryptPromise,
304
305 randomBytesPromise,
306
307 generateRSAKeyPairPromise,
308 generateED25519KeyPairPromise,
309
310 execPromise2,
311 execPromise,
312 pipelinePromise,
313
314 parseSemVersion
315 }