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