]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
WIP plugins: add plugin settings/uninstall in client
[github/Chocobozzz/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 }