-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathutils.js
173 lines (154 loc) · 4.61 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const crypto = require('node:crypto')
/**
*
* @param {string} value
* @param {string[]} criteria
* @returns {boolean}
*/
exports.hasMatch = (value, criteria) => {
return criteria.some((i) => {
return value === i || (new RegExp(i)).test(value)
})
}
/**
*
* @param {object} data
* @returns {string}
*/
exports.jsonStringify = (data) => {
const cache = []
return JSON.stringify(data, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Circular reference found, discard key
return
}
cache.push(value)
}
return value
})
}
// all paths are assumed to be '/' prepended
/**
* Returns a url builder
*
* @param {object} options companion options
*/
module.exports.getURLBuilder = (options) => {
/**
* Builds companion targeted url
*
* @param {string} path the tail path of the url
* @param {boolean} isExternal if the url is for the external world
* @param {boolean} [excludeHost] if the server domain and protocol should be included
*/
const buildURL = (path, isExternal, excludeHost) => {
let url = path
// supports for no path specified too
if (isExternal) {
url = `${options.server.implicitPath || ''}${url}`
}
url = `${options.server.path || ''}${url}`
if (!excludeHost) {
url = `${options.server.protocol}://${options.server.host}${url}`
}
return url
}
return buildURL
}
/**
* Ensure that a user-provided `secret` is 32 bytes long (the length required
* for an AES256 key) by hashing it with SHA256.
*
* @param {string|Buffer} secret
*/
function createSecret (secret) {
const hash = crypto.createHash('sha256')
hash.update(secret)
return hash.digest()
}
/**
* Create an initialization vector for AES256.
*
* @returns {Buffer}
*/
function createIv () {
return crypto.randomBytes(16)
}
function urlEncode (unencoded) {
return unencoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '~')
}
function urlDecode (encoded) {
encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/~/g, '=')
return encoded
}
/**
* Encrypt a buffer or string with AES256 and a random iv.
*
* @param {string} input
* @param {string|Buffer} secret
* @returns {string} Ciphertext as a hex string, prefixed with 32 hex characters containing the iv.
*/
module.exports.encrypt = (input, secret) => {
const iv = createIv()
const cipher = crypto.createCipheriv('aes256', createSecret(secret), iv)
let encrypted = cipher.update(input, 'utf8', 'base64')
encrypted += cipher.final('base64')
// add iv to encrypted string to use for decryption
return iv.toString('hex') + urlEncode(encrypted)
}
/**
* Decrypt an iv-prefixed or string with AES256. The iv should be in the first 32 hex characters.
*
* @param {string} encrypted
* @param {string|Buffer} secret
* @returns {string} Decrypted value.
*/
module.exports.decrypt = (encrypted, secret) => {
// Need at least 32 chars for the iv
if (encrypted.length < 32) {
throw new Error('Invalid encrypted value. Maybe it was generated with an old Companion version?')
}
const iv = Buffer.from(encrypted.slice(0, 32), 'hex')
const encryptionWithoutIv = encrypted.slice(32)
let decipher
try {
decipher = crypto.createDecipheriv('aes256', createSecret(secret), iv)
} catch (err) {
if (err.code === 'ERR_CRYPTO_INVALID_IV') {
throw new Error('Invalid initialization vector')
} else {
throw err
}
}
let decrypted = decipher.update(urlDecode(encryptionWithoutIv), 'base64', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
module.exports.defaultGetKey = (req, filename) => `${crypto.randomUUID()}-${filename}`
module.exports.prepareStream = async (stream) => new Promise((resolve, reject) => (
stream
.on('response', () => {
// Don't allow any more data to flow yet.
// /~https://github.com/request/request/issues/1990#issuecomment-184712275
stream.pause()
resolve()
})
.on('error', (err) => {
// got doesn't parse body as JSON on http error (responseType: 'json' is ignored and it instead becomes a string)
if (err?.request?.options?.responseType === 'json' && typeof err?.response?.body === 'string') {
try {
// todo unit test this
reject(Object.assign(new Error(), { response: { body: JSON.parse(err.response.body) } }))
} catch (err2) {
reject(err)
}
} else {
reject(err)
}
})
))
module.exports.getBasicAuthHeader = (key, secret) => {
const base64 = Buffer.from(`${key}:${secret}`, 'binary').toString('base64')
return `Basic ${base64}`
}