Fix search with bad webfinger handles
[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
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 root () {
138   // We are in /helpers/utils.js
139   const paths = [ __dirname, '..', '..' ]
140
141   // We are under /dist directory
142   if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
143     paths.push('..')
144   }
145
146   return join.apply(null, paths)
147 }
148
149 // Thanks: https://stackoverflow.com/a/12034334
150 function escapeHTML (stringParam) {
151   if (!stringParam) return ''
152
153   const entityMap = {
154     '&': '&',
155     '<': '&lt;',
156     '>': '&gt;',
157     '"': '&quot;',
158     '\'': '&#39;',
159     '/': '&#x2F;',
160     '`': '&#x60;',
161     '=': '&#x3D;'
162   }
163
164   return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
165 }
166
167 function pageToStartAndCount (page: number, itemsPerPage: number) {
168   const start = (page - 1) * itemsPerPage
169
170   return { start, count: itemsPerPage }
171 }
172
173 function buildPath (path: string) {
174   if (isAbsolute(path)) return path
175
176   return join(root(), path)
177 }
178
179 // Consistent with .length, lodash truncate function is not
180 function peertubeTruncate (str: string, maxLength: number) {
181   const options = {
182     length: maxLength
183   }
184   const truncatedStr = truncate(str, options)
185
186   // The truncated string is okay, we can return it
187   if (truncatedStr.length <= maxLength) return truncatedStr
188
189   // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
190   // We always use the .length so we need to truncate more if needed
191   options.length -= truncatedStr.length - maxLength
192   return truncate(str, options)
193 }
194
195 function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
196   return createHash('sha256').update(str).digest(encoding)
197 }
198
199 function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
200   return createHash('sha1').update(str).digest(encoding)
201 }
202
203 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
204   return function promisified (): Promise<A> {
205     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
206       func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
207     })
208   }
209 }
210
211 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
212 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
213   return function promisified (arg: T): Promise<A> {
214     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
215       func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
216     })
217   }
218 }
219
220 function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
221   return function promisified (arg: T): Promise<void> {
222     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
223       func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
224     })
225   }
226 }
227
228 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
229   return function promisified (arg1: T, arg2: U): Promise<A> {
230     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
231       func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
232     })
233   }
234 }
235
236 function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
237   return function promisified (arg1: T, arg2: U): Promise<void> {
238     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
239       func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
240     })
241   }
242 }
243
244 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
245 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
246 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
247 const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
248 const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
249 const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
250 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
251 const execPromise2 = promisify2<string, any, string>(exec)
252 const execPromise = promisify1<string, string>(exec)
253
254 // ---------------------------------------------------------------------------
255
256 export {
257   isTestInstance,
258   isProdInstance,
259
260   objectConverter,
261   root,
262   escapeHTML,
263   pageToStartAndCount,
264   sanitizeUrl,
265   sanitizeHost,
266   buildPath,
267   peertubeTruncate,
268
269   sha256,
270   sha1,
271
272   promisify0,
273   promisify1,
274
275   pseudoRandomBytesPromise,
276   createPrivateKey,
277   getPublicKey,
278   bcryptComparePromise,
279   bcryptGenSaltPromise,
280   bcryptHashPromise,
281   createTorrentPromise,
282   execPromise2,
283   execPromise
284 }