@tryghost/domain-validator 1.6.0 → 3.0.0
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/LICENSE +1 -1
- package/README.md +1 -1
- package/lib/caa.js +37 -0
- package/lib/pro.js +6 -6
- package/lib/request.js +23 -13
- package/lib/validate.js +42 -9
- package/package.json +10 -10
package/LICENSE
CHANGED
package/README.md
CHANGED
package/lib/caa.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const dns = require('@tryghost/dns');
|
|
2
|
+
const debug = require('@tryghost/debug')('dv:caa');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if the domain's CAA records allow the given issuer.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} customdomain
|
|
8
|
+
* @param {string} issuer - CA identity to check (e.g. 'certainly.com')
|
|
9
|
+
* @returns {Promise<{isCaaValid: boolean, caaSource: string|null, caaAllowedIssuers: string[], caaError: string|null}>}
|
|
10
|
+
*/
|
|
11
|
+
module.exports.validate = (customdomain, issuer) => {
|
|
12
|
+
debug('Checking CAA for ' + customdomain + ' against ' + issuer);
|
|
13
|
+
return dns.caa(customdomain, {issuer}).then((result) => {
|
|
14
|
+
return {
|
|
15
|
+
isCaaValid: result.allowed,
|
|
16
|
+
caaSource: result.source,
|
|
17
|
+
caaAllowedIssuers: result.allowedIssuers,
|
|
18
|
+
caaError: result.error
|
|
19
|
+
};
|
|
20
|
+
}).catch((err) => {
|
|
21
|
+
// Fail open: dns.caa fails open internally on DNS errors, but guard
|
|
22
|
+
// against unexpected throws (bad arguments, future regressions) so
|
|
23
|
+
// CAA can never take down the rest of the validity report. Tolerate
|
|
24
|
+
// non-Error rejections (null/undefined/strings) so this handler
|
|
25
|
+
// itself can never throw.
|
|
26
|
+
const message = (err && typeof err.message === 'string')
|
|
27
|
+
? err.message
|
|
28
|
+
: String(err || 'UNKNOWN');
|
|
29
|
+
debug('Unexpected CAA error: ' + message);
|
|
30
|
+
return {
|
|
31
|
+
isCaaValid: true,
|
|
32
|
+
caaSource: null,
|
|
33
|
+
caaAllowedIssuers: [],
|
|
34
|
+
caaError: message
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
};
|
package/lib/pro.js
CHANGED
|
@@ -150,19 +150,19 @@ _private.isCertificateError = error => /certificate/.test(error.context);
|
|
|
150
150
|
|
|
151
151
|
// Checks a Pro site using the provided details, and returning a responseObject
|
|
152
152
|
module.exports.validate = (proto, subdomain, customdomain, subdirectory, prodomain) => {
|
|
153
|
-
const
|
|
153
|
+
const firstAttemptUrl = _private.buildUrl(proto, customdomain, subdirectory);
|
|
154
154
|
|
|
155
|
-
debug('Validating ' +
|
|
156
|
-
return request.domain(
|
|
155
|
+
debug('Validating ' + firstAttemptUrl);
|
|
156
|
+
return request.domain(firstAttemptUrl)
|
|
157
157
|
.then((result) => {
|
|
158
158
|
debug('Response received');
|
|
159
159
|
return _private.buildResponse(result, proto, subdomain, customdomain, subdirectory, prodomain);
|
|
160
160
|
})
|
|
161
161
|
.catch(() => {
|
|
162
|
-
const
|
|
162
|
+
const secondAttemptUrl = _private.buildUrl('http', customdomain, subdirectory);
|
|
163
163
|
|
|
164
|
-
debug('Validating ' +
|
|
165
|
-
return request.domain(
|
|
164
|
+
debug('Validating ' + secondAttemptUrl);
|
|
165
|
+
return request.domain(secondAttemptUrl)
|
|
166
166
|
.then((result) => {
|
|
167
167
|
debug('HTTP response received');
|
|
168
168
|
const response = _private.buildResponse(result, 'http', subdomain, customdomain, subdirectory, prodomain);
|
package/lib/request.js
CHANGED
|
@@ -18,7 +18,7 @@ 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(new
|
|
21
|
+
return callback(new errors.InternalServerError({message: `Unable to resolve ${hostname}`}));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
if (nodeVersion >= 20) {
|
|
@@ -34,20 +34,27 @@ const staticLookup = () => async (hostname, _options, callback) => {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
module.exports.domain = (url) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @type {import('got').Options}
|
|
39
|
+
*/
|
|
40
40
|
const options = {
|
|
41
41
|
// We want to capture details of different hops
|
|
42
42
|
followRedirect: false,
|
|
43
43
|
// Don't allow the request to go on for more than 15 seconds
|
|
44
|
-
timeout:
|
|
44
|
+
timeout: {
|
|
45
|
+
request: 15000
|
|
46
|
+
},
|
|
45
47
|
// If the network errors, retry 1 time:
|
|
46
|
-
|
|
48
|
+
retry: {
|
|
49
|
+
limit: 1
|
|
50
|
+
},
|
|
47
51
|
headers: {
|
|
48
52
|
'X-Ghost-Proxy': true
|
|
49
53
|
},
|
|
50
|
-
agent:
|
|
54
|
+
agent: {
|
|
55
|
+
http: new http.Agent({lookup: staticLookup()}),
|
|
56
|
+
https: new https.Agent({lookup: staticLookup()})
|
|
57
|
+
}
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
const responses = [];
|
|
@@ -59,11 +66,11 @@ module.exports.domain = (url) => {
|
|
|
59
66
|
const statusCode = res.statusCode;
|
|
60
67
|
const headers = {};
|
|
61
68
|
|
|
62
|
-
_.reduce(res.headers, (
|
|
69
|
+
_.reduce(res.headers, (updatedHeaders = {}, value, name) => {
|
|
63
70
|
if (_.startsWith(name, 'x-ghost') || _.includes(['server', 'location', 'x-amz-cf-id'], name)) {
|
|
64
|
-
|
|
71
|
+
updatedHeaders[name] = value;
|
|
65
72
|
}
|
|
66
|
-
return
|
|
73
|
+
return updatedHeaders;
|
|
67
74
|
}, headers);
|
|
68
75
|
|
|
69
76
|
responses.push({statusCode, headers});
|
|
@@ -86,9 +93,12 @@ module.exports.domain = (url) => {
|
|
|
86
93
|
.then(handleResponse)
|
|
87
94
|
.then(() => resolve(responses))
|
|
88
95
|
.catch((error) => {
|
|
89
|
-
reject(new errors.BadRequestError(
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
reject(new errors.BadRequestError({
|
|
97
|
+
message: 'Domain could not be requested',
|
|
98
|
+
property: url,
|
|
99
|
+
context: error.message,
|
|
100
|
+
code: error.code
|
|
101
|
+
}));
|
|
92
102
|
});
|
|
93
103
|
});
|
|
94
104
|
};
|
package/lib/validate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const dns = require('./dns');
|
|
3
3
|
const pro = require('./pro');
|
|
4
|
+
const caa = require('./caa');
|
|
4
5
|
const debug = require('@tryghost/debug')('dv:validate');
|
|
5
6
|
|
|
6
7
|
const _private = {};
|
|
@@ -22,20 +23,28 @@ const validityReportObject = {
|
|
|
22
23
|
dnsRating: null,
|
|
23
24
|
isDnsValidForSSL: false,
|
|
24
25
|
sslRating: null,
|
|
26
|
+
isCaaValid: true,
|
|
27
|
+
caaSource: null,
|
|
28
|
+
caaAllowedIssuers: [],
|
|
29
|
+
caaError: null,
|
|
25
30
|
message: ''
|
|
26
31
|
};
|
|
27
32
|
|
|
28
|
-
_private.doChecks = (subdomain, customdomain, subdirectory, prodomain) => {
|
|
33
|
+
_private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuer) => {
|
|
29
34
|
const checkResult = {};
|
|
30
35
|
debug('starting pro');
|
|
31
36
|
return pro.validate('https', subdomain, customdomain, subdirectory, prodomain)
|
|
32
37
|
.then((proResult) => {
|
|
33
38
|
checkResult.pro = proResult;
|
|
34
39
|
debug('got pro, starting dns');
|
|
35
|
-
return
|
|
40
|
+
return Promise.all([
|
|
41
|
+
dns.validate(subdomain, customdomain, checkResult.pro.isGhostPro, prodomain),
|
|
42
|
+
caaIssuer ? caa.validate(customdomain, caaIssuer) : Promise.resolve(null)
|
|
43
|
+
]);
|
|
36
44
|
})
|
|
37
|
-
.then((dnsResult) => {
|
|
45
|
+
.then(([dnsResult, caaResult]) => {
|
|
38
46
|
checkResult.dns = dnsResult;
|
|
47
|
+
checkResult.caa = caaResult;
|
|
39
48
|
debug('got dns, returning');
|
|
40
49
|
return checkResult;
|
|
41
50
|
});
|
|
@@ -43,16 +52,23 @@ _private.doChecks = (subdomain, customdomain, subdirectory, prodomain) => {
|
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* check health
|
|
46
|
-
* @param {Object} blog
|
|
47
55
|
*
|
|
48
56
|
* - returns a validity report
|
|
49
57
|
* - does NOT error unless something goes wrong
|
|
58
|
+
*
|
|
59
|
+
* @param {string} subdomain
|
|
60
|
+
* @param {string} customdomain
|
|
61
|
+
* @param {string} subdirectory
|
|
62
|
+
* @param {string} [prodomain=ghost.io]
|
|
63
|
+
* @param {string} [caaIssuer] - CA identity to check CAA records against (e.g. 'certainly.com').
|
|
64
|
+
* When omitted, CAA is not checked and isCaaValid defaults to true.
|
|
50
65
|
*/
|
|
51
|
-
module.exports.validate = (subdomain, customdomain, subdirectory, prodomain = 'ghost.io') => _private
|
|
52
|
-
.doChecks(subdomain, customdomain, subdirectory, prodomain)
|
|
66
|
+
module.exports.validate = (subdomain, customdomain, subdirectory, prodomain = 'ghost.io', caaIssuer) => _private
|
|
67
|
+
.doChecks(subdomain, customdomain, subdirectory, prodomain, caaIssuer)
|
|
53
68
|
.then(function (result) {
|
|
54
|
-
//
|
|
55
|
-
|
|
69
|
+
// Deep clone so nested mutable fields (input, caaAllowedIssuers)
|
|
70
|
+
// can't leak across validation calls via the shared template.
|
|
71
|
+
const validityReport = _.cloneDeep(validityReportObject);
|
|
56
72
|
|
|
57
73
|
// Store the inputted details
|
|
58
74
|
validityReport.input.subdomain = subdomain;
|
|
@@ -64,8 +80,25 @@ module.exports.validate = (subdomain, customdomain, subdirectory, prodomain = 'g
|
|
|
64
80
|
// Do a few last DNS-related checks, and then output validity values
|
|
65
81
|
_.merge(validityReport, _.omit(result.dns, 'raw'));
|
|
66
82
|
|
|
83
|
+
if (result.caa) {
|
|
84
|
+
_.merge(validityReport, result.caa);
|
|
85
|
+
|
|
86
|
+
// CAA blocking issuance means SSL can't be provisioned going forward,
|
|
87
|
+
// even if DNS currently points at Ghost — treat it like a DNS failure.
|
|
88
|
+
if (!validityReport.isCaaValid) {
|
|
89
|
+
validityReport.isDnsValidForSSL = false;
|
|
90
|
+
validityReport.sslRating = 'D';
|
|
91
|
+
validityReport.message = 'CAA record does not allow ' + caaIssuer
|
|
92
|
+
+ ' (allowed: ' + (validityReport.caaAllowedIssuers.join(', ') || 'none')
|
|
93
|
+
+ ', source: ' + validityReport.caaSource + ')';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
if (validityReport.isProxy) {
|
|
68
|
-
|
|
98
|
+
const proxyMessage = validityReport.isProxyValid ? 'Found a valid proxy' : 'Found invalid proxy config';
|
|
99
|
+
validityReport.message = validityReport.message
|
|
100
|
+
? validityReport.message + '; ' + proxyMessage
|
|
101
|
+
: proxyMessage;
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
validityReport.raw = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/domain-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "ssh://git@github.com/TryGhost/Pro",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"test": "NODE_ENV=testing mocha -- $(find test -name '*.test.js')"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"mocha": "
|
|
28
|
+
"mocha": "11.7.5",
|
|
29
29
|
"should": "13.2.3",
|
|
30
|
-
"sinon": "
|
|
30
|
+
"sinon": "21.0.3"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@tryghost/debug": "0.
|
|
34
|
-
"@tryghost/dns": "^0.
|
|
35
|
-
"@tryghost/errors": "
|
|
36
|
-
"@tryghost/pretty-cli": "
|
|
33
|
+
"@tryghost/debug": "2.0.3",
|
|
34
|
+
"@tryghost/dns": "^0.7.0",
|
|
35
|
+
"@tryghost/errors": "3.0.3",
|
|
36
|
+
"@tryghost/pretty-cli": "3.0.3",
|
|
37
37
|
"bluebird": "3.7.2",
|
|
38
|
-
"got": "8.
|
|
39
|
-
"lodash": "4.17.
|
|
38
|
+
"got": "11.8.6",
|
|
39
|
+
"lodash": "4.17.23"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "c5d1ebb864969de89c1e8a8dcac3f577bceee1dd"
|
|
42
42
|
}
|