@tryghost/domain-validator 3.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/bin/dv.js +5 -6
- package/bin/req.js +7 -5
- package/lib/caa.js +35 -29
- package/lib/dns.js +31 -28
- package/lib/pro.js +76 -44
- package/lib/request.js +36 -25
- package/lib/validate.js +63 -44
- package/package.json +21 -21
package/README.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# Domain Validator
|
|
2
2
|
|
|
3
3
|
## Installation
|
|
4
|
+
|
|
4
5
|
1. Make sure that `gstenv` is green. See [Dev Environment](https://github.com/TryGhost/Team/blob/master/Engineering/Dev%20Environment.md) for docs.
|
|
5
6
|
2. `git clone` this repo & `cd` into it as usual
|
|
6
7
|
3. Run `yarn` to install top-level dependencies.
|
|
7
8
|
|
|
8
9
|
## Run
|
|
10
|
+
|
|
9
11
|
- Use: `yarn dev`
|
|
10
12
|
- View: [http://localhost:9999](http://localhost:9999)
|
|
11
13
|
|
|
12
14
|
## Test
|
|
15
|
+
|
|
13
16
|
- `yarn lint` run just eslint
|
|
14
17
|
- `yarn test` run lint && tests
|
|
15
18
|
|
|
16
19
|
## Publish
|
|
20
|
+
|
|
17
21
|
- `yarn ship`
|
|
18
22
|
|
|
19
23
|
# Copyright & License
|
package/bin/dv.js
CHANGED
|
@@ -3,7 +3,7 @@ const validator = require('../');
|
|
|
3
3
|
|
|
4
4
|
prettyCLI
|
|
5
5
|
.configure({
|
|
6
|
-
name: 'dv'
|
|
6
|
+
name: 'dv',
|
|
7
7
|
})
|
|
8
8
|
.groupOrder([
|
|
9
9
|
'Sources:',
|
|
@@ -12,20 +12,19 @@ prettyCLI
|
|
|
12
12
|
'Arguments:',
|
|
13
13
|
'Required Options:',
|
|
14
14
|
'Options:',
|
|
15
|
-
'Global Options:'
|
|
15
|
+
'Global Options:',
|
|
16
16
|
])
|
|
17
17
|
.positional('<domain>', {
|
|
18
18
|
paramsDesc: 'The domain you want to check',
|
|
19
|
-
mustExist: true
|
|
19
|
+
mustExist: true,
|
|
20
20
|
})
|
|
21
21
|
.positional('<subdomain>', {
|
|
22
22
|
paramsDesc: 'The expected Ghost(Pro) subdomain',
|
|
23
|
-
mustExist: true
|
|
23
|
+
mustExist: true,
|
|
24
24
|
})
|
|
25
25
|
.parseAndExit()
|
|
26
26
|
.then((argv) => {
|
|
27
27
|
validator
|
|
28
28
|
.validate(argv.subdomain, argv.domain)
|
|
29
|
-
.then(report => prettyCLI.ui.log(require('util').inspect(report, false, null))
|
|
30
|
-
);
|
|
29
|
+
.then((report) => prettyCLI.ui.log(require('util').inspect(report, false, null)));
|
|
31
30
|
});
|
package/bin/req.js
CHANGED
|
@@ -3,7 +3,7 @@ const request = require('../lib/request');
|
|
|
3
3
|
|
|
4
4
|
prettyCLI
|
|
5
5
|
.configure({
|
|
6
|
-
name: 'req'
|
|
6
|
+
name: 'req',
|
|
7
7
|
})
|
|
8
8
|
.groupOrder([
|
|
9
9
|
'Sources:',
|
|
@@ -12,16 +12,18 @@ prettyCLI
|
|
|
12
12
|
'Arguments:',
|
|
13
13
|
'Required Options:',
|
|
14
14
|
'Options:',
|
|
15
|
-
'Global Options:'
|
|
15
|
+
'Global Options:',
|
|
16
16
|
])
|
|
17
17
|
.positional('<domain>', {
|
|
18
18
|
paramsDesc: 'The domain you want to request',
|
|
19
|
-
mustExist: true
|
|
19
|
+
mustExist: true,
|
|
20
20
|
})
|
|
21
21
|
.parseAndExit()
|
|
22
22
|
.then((argv) => {
|
|
23
23
|
request
|
|
24
24
|
.domain(argv.domain)
|
|
25
|
-
.then(res => prettyCLI.ui.log('result', require('util').inspect(res, false, null)))
|
|
26
|
-
.catch(err =>
|
|
25
|
+
.then((res) => prettyCLI.ui.log('result', require('util').inspect(res, false, null)))
|
|
26
|
+
.catch((err) =>
|
|
27
|
+
prettyCLI.ui.log.error('Error', require('util').inspect(err, false, null)),
|
|
28
|
+
);
|
|
27
29
|
});
|
package/lib/caa.js
CHANGED
|
@@ -2,36 +2,42 @@ const dns = require('@tryghost/dns');
|
|
|
2
2
|
const debug = require('@tryghost/debug')('dv:caa');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Check
|
|
5
|
+
* Check whether the domain's CAA records allow any of the given issuers.
|
|
6
6
|
*
|
|
7
7
|
* @param {string} customdomain
|
|
8
|
-
* @param {string}
|
|
9
|
-
* @returns {Promise<{isCaaValid: boolean, caaSource: string|null, caaAllowedIssuers: string[], caaError: string|null}>}
|
|
8
|
+
* @param {string[]} issuers - CA identities in preference order (e.g. ['certainly.com', 'letsencrypt.org'])
|
|
9
|
+
* @returns {Promise<{isCaaValid: boolean, caaChosenIssuer: string|null, caaSource: string|null, caaAllowedIssuers: string[], caaError: string|null}>}
|
|
10
10
|
*/
|
|
11
|
-
module.exports.validate = (customdomain,
|
|
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
|
-
|
|
11
|
+
module.exports.validate = (customdomain, issuers) => {
|
|
12
|
+
// Tolerate a missing issuers arg here so we still enter the .catch below
|
|
13
|
+
// and fail open. dns.caa will throw a clear ValidationError on bad input.
|
|
14
|
+
debug('Checking CAA for ' + customdomain + ' against ' + (issuers || []).join(', '));
|
|
15
|
+
return dns
|
|
16
|
+
.caa(customdomain, { issuers })
|
|
17
|
+
.then((result) => {
|
|
18
|
+
return {
|
|
19
|
+
isCaaValid: result.allowed,
|
|
20
|
+
caaChosenIssuer: result.matchedIssuer,
|
|
21
|
+
caaSource: result.source,
|
|
22
|
+
caaAllowedIssuers: result.allowedIssuers,
|
|
23
|
+
caaError: result.error,
|
|
24
|
+
};
|
|
25
|
+
})
|
|
26
|
+
.catch((err) => {
|
|
27
|
+
// Fail open: dns.caa fails open internally on DNS errors, but guard
|
|
28
|
+
// against unexpected throws (bad arguments, future regressions) so
|
|
29
|
+
// CAA can never take down the rest of the validity report. Tolerate
|
|
30
|
+
// non-Error rejections (null/undefined/strings) so this handler
|
|
31
|
+
// itself can never throw.
|
|
32
|
+
const message =
|
|
33
|
+
err && typeof err.message === 'string' ? err.message : String(err || 'UNKNOWN');
|
|
34
|
+
debug('Unexpected CAA error: ' + message);
|
|
35
|
+
return {
|
|
36
|
+
isCaaValid: true,
|
|
37
|
+
caaChosenIssuer: issuers && issuers[0] ? issuers[0] : null,
|
|
38
|
+
caaSource: null,
|
|
39
|
+
caaAllowedIssuers: [],
|
|
40
|
+
caaError: message,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
37
43
|
};
|
package/lib/dns.js
CHANGED
|
@@ -10,7 +10,7 @@ const responseObject = {
|
|
|
10
10
|
isDnsValidForSSL: false,
|
|
11
11
|
sslRating: null,
|
|
12
12
|
message: null,
|
|
13
|
-
raw: null
|
|
13
|
+
raw: null,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// Ghost(Pro) specific logic, using extra values
|
|
@@ -20,28 +20,32 @@ _private.handleDnsReport = function handleDnsReport(dnsResult, subdomain, isGhos
|
|
|
20
20
|
const response = _.clone(responseObject);
|
|
21
21
|
|
|
22
22
|
switch (dnsResult.validity.dns) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
// CASE: correct - we have a Ghost(Pro) subdomain, check it against what we expect
|
|
24
|
+
case 'correct':
|
|
25
|
+
// CASE: mismatch - there is a CNAME, but it's not what we expected
|
|
26
|
+
if (cname !== subdomain) {
|
|
27
|
+
// CNAME is incorrect, revise the validity
|
|
28
|
+
dnsResult.validity.dns = 'mismatch';
|
|
29
|
+
dnsResult.validity.ssl = 'D';
|
|
30
|
+
dnsResult.validity.reason =
|
|
31
|
+
`CNAME mismatch, ${prodomain} subdomain is incorrect.` +
|
|
32
|
+
' Expected: ' +
|
|
33
|
+
subdomain +
|
|
34
|
+
' Actual: ' +
|
|
35
|
+
cname;
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
// CASE: obfuscated - we need to do one last check before giving up on this!
|
|
39
|
+
case 'obfuscated':
|
|
40
|
+
if (isGhostPro) {
|
|
41
|
+
dnsResult.validity.ssl = 'B';
|
|
42
|
+
dnsResult.validity.reason = 'DNS is obfuscated, but does resolve to Ghost(Pro)';
|
|
43
|
+
} else {
|
|
44
|
+
dnsResult.validity.ssl = 'D';
|
|
45
|
+
dnsResult.validity.dns = 'missing';
|
|
46
|
+
dnsResult.validity.reason = 'DNS is obfuscated and does not resolve to Ghost(Pro)';
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
response.isDnsValid = dnsResult.validity.dns === 'correct';
|
|
@@ -56,9 +60,8 @@ _private.handleDnsReport = function handleDnsReport(dnsResult, subdomain, isGhos
|
|
|
56
60
|
|
|
57
61
|
module.exports.validate = (subdomain, customdomain, isGhostPro, prodomain) => {
|
|
58
62
|
debug('Validating ' + customdomain);
|
|
59
|
-
return dns(customdomain, {cname: prodomain})
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
63
|
+
return dns(customdomain, { cname: prodomain }).then((dnsResult) => {
|
|
64
|
+
debug('Response received');
|
|
65
|
+
return _private.handleDnsReport(dnsResult, subdomain, isGhostPro, prodomain);
|
|
66
|
+
});
|
|
64
67
|
};
|
package/lib/pro.js
CHANGED
|
@@ -13,18 +13,21 @@ const responseObject = {
|
|
|
13
13
|
isProxy: false,
|
|
14
14
|
isProxyValid: null,
|
|
15
15
|
// [proxyErrors], optional
|
|
16
|
-
raw: null
|
|
16
|
+
raw: null,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
// Attempt to detect if the results look like a proxy
|
|
20
20
|
_private.detectProxy = (headers, proto) => {
|
|
21
21
|
if (
|
|
22
22
|
// If x-forwarded-host was originally set, this is a proxy. "- [Invalid]" means original value was empty
|
|
23
|
-
(_.has(headers, 'x-ghost-forwarded-host') &&
|
|
23
|
+
(_.has(headers, 'x-ghost-forwarded-host') &&
|
|
24
|
+
headers['x-ghost-forwarded-host'] !== '- [Invalid]') ||
|
|
24
25
|
// If x-amz-cf-id is set, then this is cloudfront and therefore is a proxy
|
|
25
|
-
|
|
26
|
+
_.has(headers, 'x-amz-cf-id') ||
|
|
26
27
|
// If we are making an https request and get http back, this must be a proxy
|
|
27
|
-
|
|
28
|
+
(proto === 'https' &&
|
|
29
|
+
_.has(headers, 'x-ghost-forwarded-proto') &&
|
|
30
|
+
headers['x-ghost-forwarded-proto'] !== proto)
|
|
28
31
|
) {
|
|
29
32
|
return true;
|
|
30
33
|
}
|
|
@@ -35,43 +38,49 @@ _private.detectProxy = (headers, proto) => {
|
|
|
35
38
|
_private.validateProxy = (headers, subdomain, customdomain, subdirectory, prodomain) => {
|
|
36
39
|
const proxyResult = {
|
|
37
40
|
isValid: true,
|
|
38
|
-
errors: []
|
|
41
|
+
errors: [],
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
// Forwarded Host must match the custom domain
|
|
42
|
-
if (
|
|
45
|
+
if (
|
|
46
|
+
!_.has(headers, 'x-ghost-forwarded-host') ||
|
|
47
|
+
headers['x-ghost-forwarded-host'] !== customdomain
|
|
48
|
+
) {
|
|
43
49
|
proxyResult.isValid = false;
|
|
44
|
-
proxyResult.errors.push(
|
|
45
|
-
{
|
|
46
|
-
'x-forwarded-host'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
50
|
+
proxyResult.errors.push({
|
|
51
|
+
'x-forwarded-host': {
|
|
52
|
+
actual: headers['x-ghost-forwarded-host'],
|
|
53
|
+
expected: customdomain,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Proto must be https
|
|
54
|
-
if (
|
|
59
|
+
if (
|
|
60
|
+
!_.has(headers, 'x-ghost-forwarded-proto') ||
|
|
61
|
+
headers['x-ghost-forwarded-proto'] !== 'https'
|
|
62
|
+
) {
|
|
55
63
|
proxyResult.isValid = false;
|
|
56
|
-
proxyResult.errors.push(
|
|
57
|
-
{
|
|
58
|
-
'x-forwarded-proto'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
64
|
+
proxyResult.errors.push({
|
|
65
|
+
'x-forwarded-proto': {
|
|
66
|
+
actual: headers['x-ghost-forwarded-proto'],
|
|
67
|
+
expected: 'https',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
// Host must match the pro domain
|
|
66
|
-
if (
|
|
73
|
+
if (
|
|
74
|
+
!_.has(headers, 'x-ghost-host') ||
|
|
75
|
+
headers['x-ghost-host'] !== `${subdomain}.${prodomain}`
|
|
76
|
+
) {
|
|
67
77
|
proxyResult.isValid = false;
|
|
68
|
-
proxyResult.errors.push(
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
78
|
+
proxyResult.errors.push({
|
|
79
|
+
host: {
|
|
80
|
+
actual: headers['x-ghost-host'],
|
|
81
|
+
expected: `${subdomain}.${prodomain}`,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
// If we have a subdirectory, this _must_ be a proxy
|
|
@@ -79,8 +88,8 @@ _private.validateProxy = (headers, subdomain, customdomain, subdirectory, prodom
|
|
|
79
88
|
proxyResult.errors.push({
|
|
80
89
|
subdirectory: {
|
|
81
90
|
actual: 'invalid proxy',
|
|
82
|
-
expected: 'valid proxy'
|
|
83
|
-
}
|
|
91
|
+
expected: 'valid proxy',
|
|
92
|
+
},
|
|
84
93
|
});
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -111,7 +120,9 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
|
|
|
111
120
|
if (subdomain && prodomain) {
|
|
112
121
|
validRedirectDomains.push(`${subdomain}.${prodomain}`);
|
|
113
122
|
}
|
|
114
|
-
response.isGhostProRedirect = response.redirect.every(redirect =>
|
|
123
|
+
response.isGhostProRedirect = response.redirect.every((redirect) =>
|
|
124
|
+
validRedirectDomains.includes(new URL(redirect.headers.location).hostname),
|
|
125
|
+
);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// If we find the x-ghost-cache-status header, we're seeing Ghost(Pro)
|
|
@@ -123,12 +134,19 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
|
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
// Ensure that we only set isGhostPro to true if the redirect only uses valid domains
|
|
126
|
-
response.isGhostPro =
|
|
137
|
+
response.isGhostPro =
|
|
138
|
+
response.isGhostPro && (!response.hasRedirect || response.isGhostProRedirect);
|
|
127
139
|
|
|
128
140
|
response.isProxy = _private.detectProxy(headers, proto);
|
|
129
141
|
|
|
130
142
|
// Generate a proxy report
|
|
131
|
-
const proxyResult = _private.validateProxy(
|
|
143
|
+
const proxyResult = _private.validateProxy(
|
|
144
|
+
headers,
|
|
145
|
+
subdomain,
|
|
146
|
+
customdomain,
|
|
147
|
+
subdirectory,
|
|
148
|
+
prodomain,
|
|
149
|
+
);
|
|
132
150
|
|
|
133
151
|
// If we have a detectable proxy, or a subdirectory, we must have a valid proxy config
|
|
134
152
|
if (response.isProxy || subdirectory) {
|
|
@@ -146,29 +164,43 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
|
|
|
146
164
|
};
|
|
147
165
|
|
|
148
166
|
// Predicate check to see if we should test HTTP
|
|
149
|
-
_private.isCertificateError = error => /certificate/.test(error.context);
|
|
167
|
+
_private.isCertificateError = (error) => /certificate/.test(error.context);
|
|
150
168
|
|
|
151
169
|
// Checks a Pro site using the provided details, and returning a responseObject
|
|
152
170
|
module.exports.validate = (proto, subdomain, customdomain, subdirectory, prodomain) => {
|
|
153
171
|
const firstAttemptUrl = _private.buildUrl(proto, customdomain, subdirectory);
|
|
154
172
|
|
|
155
173
|
debug('Validating ' + firstAttemptUrl);
|
|
156
|
-
return request
|
|
174
|
+
return request
|
|
175
|
+
.domain(firstAttemptUrl)
|
|
157
176
|
.then((result) => {
|
|
158
177
|
debug('Response received');
|
|
159
|
-
return _private.buildResponse(
|
|
178
|
+
return _private.buildResponse(
|
|
179
|
+
result,
|
|
180
|
+
proto,
|
|
181
|
+
subdomain,
|
|
182
|
+
customdomain,
|
|
183
|
+
subdirectory,
|
|
184
|
+
prodomain,
|
|
185
|
+
);
|
|
160
186
|
})
|
|
161
187
|
.catch(() => {
|
|
162
188
|
const secondAttemptUrl = _private.buildUrl('http', customdomain, subdirectory);
|
|
163
189
|
|
|
164
190
|
debug('Validating ' + secondAttemptUrl);
|
|
165
|
-
return request.domain(secondAttemptUrl)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
return request.domain(secondAttemptUrl).then((result) => {
|
|
192
|
+
debug('HTTP response received');
|
|
193
|
+
const response = _private.buildResponse(
|
|
194
|
+
result,
|
|
195
|
+
'http',
|
|
196
|
+
subdomain,
|
|
197
|
+
customdomain,
|
|
198
|
+
subdirectory,
|
|
199
|
+
prodomain,
|
|
200
|
+
);
|
|
201
|
+
response.isHTTPOnly = true;
|
|
202
|
+
return response;
|
|
203
|
+
});
|
|
172
204
|
})
|
|
173
205
|
.catch((error) => {
|
|
174
206
|
const response = _.clone(responseObject);
|
package/lib/request.js
CHANGED
|
@@ -7,7 +7,7 @@ const debug = require('@tryghost/debug')('dv:request');
|
|
|
7
7
|
// Use 3rd party DNS resolver
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const https = require('https');
|
|
10
|
-
const {Resolver} = require('dns').promises;
|
|
10
|
+
const { Resolver } = require('dns').promises;
|
|
11
11
|
const resolver = new Resolver();
|
|
12
12
|
resolver.setServers(['1.1.1.1', '1.0.0.1']);
|
|
13
13
|
const semverMajor = require('semver/functions/major');
|
|
@@ -18,12 +18,14 @@ const staticLookup = () => async (hostname, _options, callback) => {
|
|
|
18
18
|
const ips = await resolver.resolve(hostname);
|
|
19
19
|
|
|
20
20
|
if (ips.length === 0) {
|
|
21
|
-
return callback(
|
|
21
|
+
return callback(
|
|
22
|
+
new errors.InternalServerError({ message: `Unable to resolve ${hostname}` }),
|
|
23
|
+
);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (nodeVersion >= 20) {
|
|
25
27
|
// Node 20+ uses a different callback signature
|
|
26
|
-
callback(null, [{address: ips[0], family: 4}]);
|
|
28
|
+
callback(null, [{ address: ips[0], family: 4 }]);
|
|
27
29
|
} else {
|
|
28
30
|
// Node 18 and below use the old callback signature
|
|
29
31
|
callback(null, ips[0], 4);
|
|
@@ -42,19 +44,19 @@ module.exports.domain = (url) => {
|
|
|
42
44
|
followRedirect: false,
|
|
43
45
|
// Don't allow the request to go on for more than 15 seconds
|
|
44
46
|
timeout: {
|
|
45
|
-
request: 15000
|
|
47
|
+
request: 15000,
|
|
46
48
|
},
|
|
47
49
|
// If the network errors, retry 1 time:
|
|
48
50
|
retry: {
|
|
49
|
-
limit: 1
|
|
51
|
+
limit: 1,
|
|
50
52
|
},
|
|
51
53
|
headers: {
|
|
52
|
-
'X-Ghost-Proxy': true
|
|
54
|
+
'X-Ghost-Proxy': true,
|
|
53
55
|
},
|
|
54
56
|
agent: {
|
|
55
|
-
http: new http.Agent({lookup: staticLookup()}),
|
|
56
|
-
https: new https.Agent({lookup: staticLookup()})
|
|
57
|
-
}
|
|
57
|
+
http: new http.Agent({ lookup: staticLookup() }),
|
|
58
|
+
https: new https.Agent({ lookup: staticLookup() }),
|
|
59
|
+
},
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const responses = [];
|
|
@@ -66,14 +68,21 @@ module.exports.domain = (url) => {
|
|
|
66
68
|
const statusCode = res.statusCode;
|
|
67
69
|
const headers = {};
|
|
68
70
|
|
|
69
|
-
_.reduce(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
_.reduce(
|
|
72
|
+
res.headers,
|
|
73
|
+
(updatedHeaders = {}, value, name) => {
|
|
74
|
+
if (
|
|
75
|
+
_.startsWith(name, 'x-ghost') ||
|
|
76
|
+
_.includes(['server', 'location', 'x-amz-cf-id'], name)
|
|
77
|
+
) {
|
|
78
|
+
updatedHeaders[name] = value;
|
|
79
|
+
}
|
|
80
|
+
return updatedHeaders;
|
|
81
|
+
},
|
|
82
|
+
headers,
|
|
83
|
+
);
|
|
75
84
|
|
|
76
|
-
responses.push({statusCode, headers});
|
|
85
|
+
responses.push({ statusCode, headers });
|
|
77
86
|
|
|
78
87
|
if (statusCode.toString()[0] === '3') {
|
|
79
88
|
numHops += 1;
|
|
@@ -82,23 +91,25 @@ module.exports.domain = (url) => {
|
|
|
82
91
|
return;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
return got.head(res.headers.location, options)
|
|
86
|
-
.then(handleResponse);
|
|
94
|
+
return got.head(res.headers.location, options).then(handleResponse);
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
return new Promise(function (resolve, reject) {
|
|
91
99
|
debug('starting request');
|
|
92
|
-
return got
|
|
100
|
+
return got
|
|
101
|
+
.head(url, options)
|
|
93
102
|
.then(handleResponse)
|
|
94
103
|
.then(() => resolve(responses))
|
|
95
104
|
.catch((error) => {
|
|
96
|
-
reject(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
reject(
|
|
106
|
+
new errors.BadRequestError({
|
|
107
|
+
message: 'Domain could not be requested',
|
|
108
|
+
property: url,
|
|
109
|
+
context: error.message,
|
|
110
|
+
code: error.code,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
102
113
|
});
|
|
103
114
|
});
|
|
104
115
|
};
|
package/lib/validate.js
CHANGED
|
@@ -11,7 +11,7 @@ const validityReportObject = {
|
|
|
11
11
|
input: {
|
|
12
12
|
subdomain: '',
|
|
13
13
|
customdomain: '',
|
|
14
|
-
subdirectory: ''
|
|
14
|
+
subdirectory: '',
|
|
15
15
|
},
|
|
16
16
|
isResponding: false,
|
|
17
17
|
isHTTPOnly: false,
|
|
@@ -24,22 +24,26 @@ const validityReportObject = {
|
|
|
24
24
|
isDnsValidForSSL: false,
|
|
25
25
|
sslRating: null,
|
|
26
26
|
isCaaValid: true,
|
|
27
|
+
caaChosenIssuer: null,
|
|
27
28
|
caaSource: null,
|
|
28
29
|
caaAllowedIssuers: [],
|
|
29
30
|
caaError: null,
|
|
30
|
-
message: ''
|
|
31
|
+
message: '',
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
_private.doChecks = (subdomain, customdomain, subdirectory, prodomain,
|
|
34
|
+
_private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuers) => {
|
|
34
35
|
const checkResult = {};
|
|
35
36
|
debug('starting pro');
|
|
36
|
-
return pro
|
|
37
|
+
return pro
|
|
38
|
+
.validate('https', subdomain, customdomain, subdirectory, prodomain)
|
|
37
39
|
.then((proResult) => {
|
|
38
40
|
checkResult.pro = proResult;
|
|
39
41
|
debug('got pro, starting dns');
|
|
40
42
|
return Promise.all([
|
|
41
43
|
dns.validate(subdomain, customdomain, checkResult.pro.isGhostPro, prodomain),
|
|
42
|
-
|
|
44
|
+
caaIssuers && caaIssuers.length > 0
|
|
45
|
+
? caa.validate(customdomain, caaIssuers)
|
|
46
|
+
: Promise.resolve(null),
|
|
43
47
|
]);
|
|
44
48
|
})
|
|
45
49
|
.then(([dnsResult, caaResult]) => {
|
|
@@ -60,51 +64,66 @@ _private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuer
|
|
|
60
64
|
* @param {string} customdomain
|
|
61
65
|
* @param {string} subdirectory
|
|
62
66
|
* @param {string} [prodomain=ghost.io]
|
|
63
|
-
* @param {string} [
|
|
64
|
-
*
|
|
67
|
+
* @param {string[]} [caaIssuers] - CA identities to check CAA records against, in preference order
|
|
68
|
+
* (e.g. ['certainly.com', 'letsencrypt.org']). When omitted or
|
|
69
|
+
* empty, CAA is not checked and isCaaValid defaults to true.
|
|
65
70
|
*/
|
|
66
|
-
module.exports.validate = (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
module.exports.validate = (
|
|
72
|
+
subdomain,
|
|
73
|
+
customdomain,
|
|
74
|
+
subdirectory,
|
|
75
|
+
prodomain = 'ghost.io',
|
|
76
|
+
caaIssuers,
|
|
77
|
+
) =>
|
|
78
|
+
_private
|
|
79
|
+
.doChecks(subdomain, customdomain, subdirectory, prodomain, caaIssuers)
|
|
80
|
+
.then(function (result) {
|
|
81
|
+
// Deep clone so nested mutable fields (input, caaAllowedIssuers)
|
|
82
|
+
// can't leak across validation calls via the shared template.
|
|
83
|
+
const validityReport = _.cloneDeep(validityReportObject);
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
// Store the inputted details
|
|
86
|
+
validityReport.input.subdomain = subdomain;
|
|
87
|
+
validityReport.input.customdomain = customdomain;
|
|
88
|
+
validityReport.input.subdirectory = subdirectory;
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
// Copy over all the values from result.pro
|
|
91
|
+
_.merge(validityReport, _.omit(result.pro, 'raw'));
|
|
92
|
+
// Do a few last DNS-related checks, and then output validity values
|
|
93
|
+
_.merge(validityReport, _.omit(result.dns, 'raw'));
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
if (result.caa) {
|
|
96
|
+
_.merge(validityReport, result.caa);
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
// CAA blocking issuance means SSL can't be provisioned going forward,
|
|
99
|
+
// even if DNS currently points at Ghost — treat it like a DNS failure.
|
|
100
|
+
if (!validityReport.isCaaValid) {
|
|
101
|
+
validityReport.isDnsValidForSSL = false;
|
|
102
|
+
validityReport.sslRating = 'D';
|
|
103
|
+
validityReport.message =
|
|
104
|
+
'CAA record does not allow any of ' +
|
|
105
|
+
caaIssuers.join(', ') +
|
|
106
|
+
' (allowed: ' +
|
|
107
|
+
(validityReport.caaAllowedIssuers.join(', ') || 'none') +
|
|
108
|
+
', source: ' +
|
|
109
|
+
validityReport.caaSource +
|
|
110
|
+
')';
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
|
-
}
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
if (validityReport.isProxy) {
|
|
115
|
+
const proxyMessage = validityReport.isProxyValid
|
|
116
|
+
? 'Found a valid proxy'
|
|
117
|
+
: 'Found invalid proxy config';
|
|
118
|
+
validityReport.message = validityReport.message
|
|
119
|
+
? validityReport.message + '; ' + proxyMessage
|
|
120
|
+
: proxyMessage;
|
|
121
|
+
}
|
|
103
122
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
validityReport.raw = {
|
|
124
|
+
pro: result.pro.raw,
|
|
125
|
+
dns: result.dns.raw,
|
|
126
|
+
};
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
return validityReport;
|
|
129
|
+
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/domain-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Ghost Foundation",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
|
-
"url": "ssh://git@github.com/TryGhost/Pro",
|
|
8
|
+
"url": "ssh://git@github.com/TryGhost/Pro-Packages",
|
|
7
9
|
"directory": "packages/domain-validator"
|
|
8
10
|
},
|
|
9
|
-
"author": "Ghost Foundation",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"main": "lib/validate.js",
|
|
12
11
|
"bin": {
|
|
13
12
|
"dv": "./bin/dv.js"
|
|
14
13
|
},
|
|
@@ -17,26 +16,27 @@
|
|
|
17
16
|
"lib",
|
|
18
17
|
"index.js"
|
|
19
18
|
],
|
|
20
|
-
"
|
|
21
|
-
"dv": "node bin/dv.js",
|
|
22
|
-
"req": "node bin/req.js",
|
|
23
|
-
"lint": "eslint . --ext .js --cache",
|
|
24
|
-
"posttest": "yarn lint",
|
|
25
|
-
"test": "NODE_ENV=testing mocha -- $(find test -name '*.test.js')"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"mocha": "11.7.5",
|
|
29
|
-
"should": "13.2.3",
|
|
30
|
-
"sinon": "21.0.3"
|
|
31
|
-
},
|
|
19
|
+
"main": "lib/validate.js",
|
|
32
20
|
"dependencies": {
|
|
33
21
|
"@tryghost/debug": "2.0.3",
|
|
34
|
-
"@tryghost/dns": "^0.7.0",
|
|
35
22
|
"@tryghost/errors": "3.0.3",
|
|
36
23
|
"@tryghost/pretty-cli": "3.0.3",
|
|
37
24
|
"bluebird": "3.7.2",
|
|
38
25
|
"got": "11.8.6",
|
|
39
|
-
"lodash": "4.17.23"
|
|
26
|
+
"lodash": "4.17.23",
|
|
27
|
+
"semver": "7.7.4",
|
|
28
|
+
"@tryghost/dns": "^0.8.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"mocha": "11.7.5",
|
|
32
|
+
"should": "13.2.3",
|
|
33
|
+
"sinon": "21.0.3"
|
|
40
34
|
},
|
|
41
|
-
"
|
|
42
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dv": "node bin/dv.js",
|
|
37
|
+
"req": "node bin/req.js",
|
|
38
|
+
"lint": "oxlint -c ../../.oxlintrc.json .",
|
|
39
|
+
"posttest": "pnpm lint",
|
|
40
|
+
"test": "NODE_ENV=testing mocha -- $(find test -name '*.test.js')"
|
|
41
|
+
}
|
|
42
|
+
}
|