codekingpro/portable-devtools
115k
1const {
2 URL_MATCHER,
3 TYPE_URL,
4 TYPE_REGEX,
5 TYPE_PATH,
6} = require('./matchers')
7
8/**
9 * creates a string of asterisks,
10 * this forces a minimum asterisk for security purposes
11 */
12const asterisk = (length = 0) => {
13 length = typeof length === 'string' ? length.length : length
14 if (length < 8) {
15 return '*'.repeat(8)
16 }
17 return '*'.repeat(length)
18}
19
20/**
21 * escapes all special regex chars
22 * @see https://stackoverflow.com/a/9310752
23 * @see https://github.com/tc39/proposal-regex-escaping
24 */
25const escapeRegExp = (text) => {
26 return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, `\\$&`)
27}
28
29/**
30 * provieds a regex "or" of the url versions of a string
31 */
32const urlEncodeRegexGroup = (value) => {
33 const decoded = decodeURIComponent(value)
34 const encoded = encodeURIComponent(value)
35 const union = [...new Set([encoded, decoded, value])].map(escapeRegExp).join('|')
36 return union
37}
38
39/**
40 * a tagged template literal that returns a regex ensures all variables are excaped
41 */
42const urlEncodeRegexTag = (strings, ...values) => {
43 let pattern = ''
44 for (let i = 0; i < values.length; i++) {
45 pattern += strings[i] + `(${urlEncodeRegexGroup(values[i])})`
46 }
47 pattern += strings[strings.length - 1]
48 return new RegExp(pattern)
49}
50
51/**
52 * creates a matcher for redacting url hostname
53 */
54const redactUrlHostnameMatcher = ({ hostname, replacement } = {}) => ({
55 type: TYPE_URL,
56 predicate: ({ url }) => url.hostname === hostname,
57 pattern: ({ url }) => {
58 return urlEncodeRegexTag`(^${url.protocol}//${url.username}:.+@)?${url.hostname}`
59 },
60 replacement: `$1${replacement || asterisk()}`,
61})
62
63/**
64 * creates a matcher for redacting url search / query parameter values
65 */
66const redactUrlSearchParamsMatcher = ({ param, replacement } = {}) => ({
67 type: TYPE_URL,
68 predicate: ({ url }) => url.searchParams.has(param),
69 pattern: ({ url }) => urlEncodeRegexTag`(${param}=)${url.searchParams.get(param)}`,
70 replacement: `$1${replacement || asterisk()}`,
71})
72
73/** creates a matcher for redacting the url password */
74const redactUrlPasswordMatcher = ({ replacement } = {}) => ({
75 type: TYPE_URL,
76 predicate: ({ url }) => url.password,
77 pattern: ({ url }) => urlEncodeRegexTag`(^${url.protocol}//${url.username}:)${url.password}`,
78 replacement: `$1${replacement || asterisk()}`,
79})
80
81const redactUrlReplacement = (...matchers) => (subValue) => {
82 try {
83 const url = new URL(subValue)
84 return redactMatchers(...matchers)(subValue, { url })
85 } catch (err) {
86 return subValue
87 }
88}
89
90/**
91 * creates a matcher / submatcher for urls, this function allows you to first
92 * collect all urls within a larger string and then pass those urls to a
93 * submatcher
94 *
95 * @example
96 * console.log("this will first match all urls, then pass those urls to the password patcher")
97 * redactMatchers(redactUrlMatcher(redactUrlPasswordMatcher()))
98 *
99 * @example
100 * console.log(
101 * "this will assume you are passing in a string that is a url, and will redact the password"
102 * )
103 * redactMatchers(redactUrlPasswordMatcher())
104 *
105 */
106const redactUrlMatcher = (...matchers) => {
107 return {
108 ...URL_MATCHER,
109 replacement: redactUrlReplacement(...matchers),
110 }
111}
112
113const matcherFunctions = {
114 [TYPE_REGEX]: (matcher) => (value) => {
115 if (typeof value === 'string') {
116 value = value.replace(matcher.pattern, matcher.replacement)
117 }
118 return value
119 },
120 [TYPE_URL]: (matcher) => (value, ctx) => {
121 if (typeof value === 'string') {
122 try {
123 const url = ctx?.url || new URL(value)
124 const { predicate, pattern } = matcher
125 const predicateValue = predicate({ url })
126 if (predicateValue) {
127 value = value.replace(pattern({ url }), matcher.replacement)
128 }
129 } catch (_e) {
130 return value
131 }
132 }
133 return value
134 },
135 [TYPE_PATH]: (matcher) => (value, ctx) => {
136 const rawPath = ctx?.path
137 const path = rawPath.join('.').toLowerCase()
138 const { predicate, replacement } = matcher
139 const replace = typeof replacement === 'function' ? replacement : () => replacement
140 const shouldRun = predicate({ rawPath, path })
141 if (shouldRun) {
142 value = replace(value, { rawPath, path })
143 }
144 return value
145 },
146}
147
148/** converts a matcher to a function */
149const redactMatcher = (matcher) => {
150 return matcherFunctions[matcher.type](matcher)
151}
152
153/** converts a series of matchers to a function */
154const redactMatchers = (...matchers) => (value, ctx) => {
155 const flatMatchers = matchers.flat()
156 return flatMatchers.reduce((result, matcher) => {
157 const fn = (typeof matcher === 'function') ? matcher : redactMatcher(matcher)
158 return fn(result, ctx)
159 }, value)
160}
161
162/**
163 * replacement handler, keeping $1 (if it exists) and replacing the
164 * rest of the string with asterisks, maintaining string length
165 */
166const redactDynamicReplacement = () => (value, start) => {
167 if (typeof start === 'number') {
168 return asterisk(value)
169 }
170 return start + asterisk(value.substring(start.length).length)
171}
172
173/**
174 * replacement handler, keeping $1 (if it exists) and replacing the
175 * rest of the string with a fixed number of asterisks
176 */
177const redactFixedReplacement = (length) => (_value, start) => {
178 if (typeof start === 'number') {
179 return asterisk(length)
180 }
181 return start + asterisk(length)
182}
183
184const redactUrlPassword = (value, replacement) => {
185 return redactMatchers(redactUrlPasswordMatcher({ replacement }))(value)
186}
187
188module.exports = {
189 asterisk,
190 escapeRegExp,
191 urlEncodeRegexGroup,
192 urlEncodeRegexTag,
193 redactUrlHostnameMatcher,
194 redactUrlSearchParamsMatcher,
195 redactUrlPasswordMatcher,
196 redactUrlMatcher,
197 redactUrlReplacement,
198 redactDynamicReplacement,
199 redactFixedReplacement,
200 redactMatchers,
201 redactUrlPassword,
202}
203 