Merge remote-tracking branch 'origin/acme_sync' into v2

# Conflicts:
#	packages/core/acme-client/package.json
This commit is contained in:
xiaojunnuo
2024-08-25 03:01:13 +08:00
9 changed files with 381 additions and 51 deletions
+87 -1
View File
@@ -3,10 +3,14 @@
*/
const axios = require('axios');
const { parseRetryAfterHeader } = require('./util');
const { log } = require('./logger');
const pkg = require('./../package.json');
const { AxiosError } = axios;
/**
* Instance
* Defaults
*/
const instance = axios.create();
@@ -19,6 +23,9 @@ instance.defaults.acmeSettings = {
httpChallengePort: 80,
httpsChallengePort: 443,
tlsAlpnChallengePort: 443,
retryMaxAttempts: 5,
retryDefaultDelay: 5,
};
// instance.defaults.proxy = {
// host: '192.168.34.139',
@@ -33,6 +40,85 @@ instance.defaults.acmeSettings = {
instance.defaults.adapter = 'http';
/**
* Retry requests on server errors or when rate limited
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
*/
function isRetryableError(error) {
return (error.code !== 'ECONNABORTED')
&& (error.code !== 'ERR_NOCK_NO_MATCH')
&& (!error.response
|| (error.response.status === 429)
|| ((error.response.status >= 500) && (error.response.status <= 599)));
}
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
function validateStatus(response) {
const validator = response.config.retryValidateStatus;
if (!response.status || !validator || validator(response.status)) {
return response;
}
throw new AxiosError(
`Request failed with status code ${response.status}`,
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
response.config,
response.request,
response,
);
}
/* Pass all responses through the error interceptor */
instance.interceptors.request.use((config) => {
if (!('retryValidateStatus' in config)) {
config.retryValidateStatus = config.validateStatus;
}
config.validateStatus = () => false;
return config;
});
/* Handle request retries if applicable */
instance.interceptors.response.use(null, async (error) => {
const { config, response } = error;
if (!config) {
return Promise.reject(error);
}
/* Pick up errors we want to retry */
if (isRetryableError(error)) {
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
if (config.retryAttempt <= retryMaxAttempts) {
const code = response ? `HTTP ${response.status}` : error.code;
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
/* Attempt to parse Retry-After header, fallback to default delay */
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
if (retryAfter > 0) {
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
}
else {
retryAfter = (retryDefaultDelay * config.retryAttempt);
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
}
/* Wait and retry the request */
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
return instance(config);
}
}
/* Validate and return response */
return validateStatus(response);
});
/**
* Export instance
*/
+5 -2
View File
@@ -93,9 +93,11 @@ class HttpClient {
*/
async getDirectory() {
const age = (Math.floor(Date.now() / 1000) - this.directoryTimestamp);
const now = Math.floor(Date.now() / 1000);
const age = (now - this.directoryTimestamp);
if (!this.directoryCache || (age > this.directoryMaxAge)) {
log(`Refreshing ACME directory, age: ${age}`);
const resp = await this.request(this.directoryUrl, 'get');
if (resp.status >= 400) {
@@ -107,6 +109,7 @@ class HttpClient {
}
this.directoryCache = resp.data;
this.directoryTimestamp = now;
}
return this.directoryCache;
@@ -131,7 +134,7 @@ class HttpClient {
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
*
* @returns {Promise<string>} nonce
* @returns {Promise<string>} Nonce
*/
async getNonce() {
+45 -8
View File
@@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
}
/**
* Parse URLs from link header
* Parse URLs from Link header
*
* @param {string} header Link header contents
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
*
* @param {string} header Header contents
* @param {string} rel Link relation, default: `alternate`
* @returns {string[]} Array of URLs
*/
@@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') {
return results.filter((r) => r);
}
/**
* Parse date or duration from Retry-After header
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
*
* @param {string} header Header contents
* @returns {number} Retry duration in seconds
*/
function parseRetryAfterHeader(header) {
const sec = parseInt(header, 10);
const date = new Date(header);
/* Seconds into the future */
if (Number.isSafeInteger(sec) && (sec > 0)) {
return sec;
}
/* Future date string */
if (date instanceof Date && !Number.isNaN(date)) {
const now = new Date();
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
if (diff > 0) {
return diff;
}
}
return 0;
}
/**
* Find certificate chain with preferred issuer common name
* - If issuer is found in multiple chains, the closest to root wins
@@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) {
function formatResponseError(resp) {
let result;
if (resp.data.error) {
result = resp.data.error.detail || resp.data.error;
}
else {
result = resp.data.detail || JSON.stringify(resp.data);
if (resp.data) {
if (resp.data.error) {
result = resp.data.error.detail || resp.data.error;
}
else {
result = resp.data.detail || JSON.stringify(resp.data);
}
}
return result.replace(/\n/g, '');
return (result || '').replace(/\n/g, '');
}
/**
@@ -296,6 +332,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
module.exports = {
retry,
parseLinkHeader,
parseRetryAfterHeader,
findCertificateChainForIssuer,
formatResponseError,
getAuthoritativeDnsResolver,