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