WIP plugins: install/uninstall
[oweals/peertube.git] / server / helpers / core-utils.ts
1 /*
2   Different from 'utils' because we don't not import other PeerTube modules.
3   Useful to avoid circular dependencies.
4 */
5
6 import * as bcrypt from 'bcrypt'
7 import * as createTorrent from 'create-torrent'
8 import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto'
9 import { isAbsolute, join } from 'path'
10 import * as pem from 'pem'
11 import { URL } from 'url'
12 import { truncate } from 'lodash'
13 import { exec, ExecOptions } from 'child_process'
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 const timeTable = {
34   ms:           1,
35   second:       1000,
36   minute:       60000,
37   hour:         3600000,
38   day:          3600000 * 24,
39   week:         3600000 * 24 * 7,
40   month:        3600000 * 24 * 30
41 }
42
43 export function parseDurationToMs (duration: number | string): number {
44   if (typeof duration === 'number') return duration
45
46   if (typeof duration === 'string') {
47     const split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
48
49     if (split.length === 3) {
50       const len = parseFloat(split[1])
51       let unit = split[2].replace(/s$/i,'').toLowerCase()
52       if (unit === 'm') {
53         unit = 'ms'
54       }
55
56       return (len || 1) * (timeTable[unit] || 0)
57     }
58   }
59
60   throw new Error(`Duration ${duration} could not be properly parsed`)
61 }
62
63 export function parseBytes (value: string | number): number {
64   if (typeof value === 'number') return value
65
66   const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
67   const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
68   const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
69   const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
70   const t = /^(\d+)\s*TB$/
71   const g = /^(\d+)\s*GB$/
72   const m = /^(\d+)\s*MB$/
73   const b = /^(\d+)\s*B$/
74   let match
75
76   if (value.match(tgm)) {
77     match = value.match(tgm)
78     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
79     + parseInt(match[2], 10) * 1024 * 1024 * 1024
80     + parseInt(match[3], 10) * 1024 * 1024
81   } else if (value.match(tg)) {
82     match = value.match(tg)
83     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
84     + parseInt(match[2], 10) * 1024 * 1024 * 1024
85   } else if (value.match(tm)) {
86     match = value.match(tm)
87     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
88     + parseInt(match[2], 10) * 1024 * 1024
89   } else if (value.match(gm)) {
90     match = value.match(gm)
91     return parseInt(match[1], 10) * 1024 * 1024 * 1024
92     + parseInt(match[2], 10) * 1024 * 1024
93   } else if (value.match(t)) {
94     match = value.match(t)
95     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
96   } else if (value.match(g)) {
97     match = value.match(g)
98     return parseInt(match[1], 10) * 1024 * 1024 * 1024
99   } else if (value.match(m)) {
100     match = value.match(m)
101     return parseInt(match[1], 10) * 1024 * 1024
102   } else if (value.match(b)) {
103     match = value.match(b)
104     return parseInt(match[1], 10) * 1024
105   } else {
106     return parseInt(value, 10)
107   }
108 }
109
110 function sanitizeUrl (url: string) {
111   const urlObject = new URL(url)
112
113   if (urlObject.protocol === 'https:' && urlObject.port === '443') {
114     urlObject.port = ''
115   } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
116     urlObject.port = ''
117   }
118
119   return urlObject.href.replace(/\/$/, '')
120 }
121
122 // Don't import remote scheme from constants because we are in core utils
123 function sanitizeHost (host: string, remoteScheme: string) {
124   const toRemove = remoteScheme === 'https' ? 443 : 80
125
126   return host.replace(new RegExp(`:${toRemove}$`), '')
127 }
128
129 function isTestInstance () {
130   return process.env.NODE_ENV === 'test'
131 }
132
133 function isProdInstance () {
134   return process.env.NODE_ENV === 'production'
135 }
136
137 function getAppNumber () {
138   return process.env.NODE_APP_INSTANCE
139 }
140
141 function root () {
142   // We are in /helpers/utils.js
143   const paths = [ __dirname, '..', '..' ]
144
145   // We are under /dist directory
146   if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
147     paths.push('..')
148   }
149
150   return join.apply(null, paths)
151 }
152
153 // Thanks: https://stackoverflow.com/a/12034334
154 function escapeHTML (stringParam) {
155   if (!stringParam) return ''
156
157   const entityMap = {
158     '&': '&',
159     '<': '&lt;',
160     '>': '&gt;',
161     '"': '&quot;',
162     '\'': '&#39;',
163     '/': '&#x2F;',
164     '`': '&#x60;',
165     '=': '&#x3D;'
166   }
167
168   return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
169 }
170
171 function pageToStartAndCount (page: number, itemsPerPage: number) {
172   const start = (page - 1) * itemsPerPage
173
174   return { start, count: itemsPerPage }
175 }
176
177 function buildPath (path: string) {
178   if (isAbsolute(path)) return path
179
180   return join(root(), path)
181 }
182
183 // Consistent with .length, lodash truncate function is not
184 function peertubeTruncate (str: string, maxLength: number) {
185   const options = {
186     length: maxLength
187   }
188   const truncatedStr = truncate(str, options)
189
190   // The truncated string is okay, we can return it
191   if (truncatedStr.length <= maxLength) 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 - maxLength
196   return truncate(str, options)
197 }
198
199 function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
200   return createHash('sha256').update(str).digest(encoding)
201 }
202
203 function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
204   return createHash('sha1').update(str).digest(encoding)
205 }
206
207 function execShell (command: string, options?: ExecOptions) {
208   return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
209     exec(command, options, (err, stdout, stderr) => {
210       if (err) return rej({ err, stdout, stderr })
211
212       return res({ stdout, stderr })
213     })
214   })
215 }
216
217 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
218   return function promisified (): Promise<A> {
219     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
220       func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
221     })
222   }
223 }
224
225 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
226 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
227   return function promisified (arg: T): Promise<A> {
228     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
229       func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
230     })
231   }
232 }
233
234 function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
235   return function promisified (arg: T): Promise<void> {
236     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
237       func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
238     })
239   }
240 }
241
242 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
243   return function promisified (arg1: T, arg2: U): Promise<A> {
244     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
245       func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
246     })
247   }
248 }
249
250 function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
251   return function promisified (arg1: T, arg2: U): Promise<void> {
252     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
253       func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
254     })
255   }
256 }
257
258 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
259 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
260 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
261 const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
262 const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
263 const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
264 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
265 const execPromise2 = promisify2<string, any, string>(exec)
266 const execPromise = promisify1<string, string>(exec)
267
268 // ---------------------------------------------------------------------------
269
270 export {
271   isTestInstance,
272   isProdInstance,
273   getAppNumber,
274
275   objectConverter,
276   root,
277   escapeHTML,
278   pageToStartAndCount,
279   sanitizeUrl,
280   sanitizeHost,
281   buildPath,
282   execShell,
283   peertubeTruncate,
284
285   sha256,
286   sha1,
287
288   promisify0,
289   promisify1,
290
291   pseudoRandomBytesPromise,
292   createPrivateKey,
293   getPublicKey,
294   bcryptComparePromise,
295   bcryptGenSaltPromise,
296   bcryptHashPromise,
297   createTorrentPromise,
298   execPromise2,
299   execPromise
300 }