Skip to content

Commit

Permalink
add Logger class (#9)
Browse files Browse the repository at this point in the history
add Logger class

This lets us swap from ngx.log to Logger, diffs like

- ngx.log(ngx.INFO, `njs-acme: [client] Item has status: ${respData.status}`)
+ log.info('Item has status:', respData.status)
  • Loading branch information
ryepup authored Jul 7, 2023
1 parent 274755f commit a9342d8
Show file tree
Hide file tree
Showing 12 changed files with 461 additions and 275 deletions.
7 changes: 6 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
"ms-vsliveshare.vsliveshare",
"ms-azuretools.vscode-docker",
"esbenp.prettier-vscode"
]
],
"settings": {
"[typescript]":{
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
}
}
1 change: 1 addition & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ module.exports = {
],
spec: [
'integration-tests/**/*.test.ts',
'unit-tests/**/*.test.ts',
],
}
1 change: 1 addition & 0 deletions .prettierrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
semi: false
singleQuote: true
trailingComma: es5
tabWidth: 2
187 changes: 123 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,79 +250,138 @@ The `clientAutoMode` exported function is a reference implementation of the `js_
* @param {NginxHTTPRequest} r Incoming request
* @returns void
*/
async function clientAutoMode(r: NginxHTTPRequest) {
const accountKey = await readOrCreateAccountKey(NJS_ACME_ACCOUNT_PRIVATE_JWK);
// /* Create a new ACME account */
let client = new AcmeClient({
directoryUrl: DIRECTORY_URL,
accountKey: accountKey
});
// client.api.setDebug(true);
client.api.setVerify(false);
const email = r.variables.njs_acme_account_email || process.env.NJS_ACME_ACCOUNT_EMAIL
if (email.length == 0) {
r.return(500,"Nginx variable 'njs_acme_account_email' or 'NJS_ACME_ACCOUNT_EMAIL' environment variable must be set")
async function clientAutoMode(r: NginxHTTPRequest): Promise<void> {
const log = new Logger('auto')
const prefix = acmeDir(r)
const serverNames = acmeServerNames(r)

const commonName = serverNames[0]
const pkeyPath = joinPaths(prefix, commonName + KEY_SUFFIX)
const csrPath = joinPaths(prefix, commonName + '.csr')
const certPath = joinPaths(prefix, commonName + CERTIFICATE_SUFFIX)

let email
try {
email = getVariable(r, 'njs_acme_account_email')
} catch {
return r.return(
500,
"Nginx variable 'njs_acme_account_email' or 'NJS_ACME_ACCOUNT_EMAIL' environment variable must be set"
)
}

let certificatePem
let pkeyPem
let renewCertificate = false
let certInfo
try {
const certData = fs.readFileSync(certPath, 'utf8')
const privateKeyData = fs.readFileSync(pkeyPath, 'utf8')

certInfo = await readCertificateInfo(certData)
// Calculate the date 30 days before the certificate expiration
const renewalThreshold = new Date(certInfo.notAfter as string)
renewalThreshold.setDate(renewalThreshold.getDate() - 30)

const currentDate = new Date()
if (currentDate > renewalThreshold) {
renewCertificate = true
} else {
certificatePem = certData
pkeyPem = privateKeyData
}
// create a new CSR
const commonName = r.variables.server_name?.toLowerCase() || r.variables.njs_acme_server_name
} catch {
renewCertificate = true
}

if (renewCertificate) {
const accountKey = await readOrCreateAccountKey(
acmeAccountPrivateJWKPath(r)
)
// Create a new ACME client
const client = new AcmeClient({
directoryUrl: acmeDirectoryURI(r),
accountKey: accountKey,
})
// client.api.minLevel = LogLevel.Debug; // display more logs
client.api.setVerify(acmeVerifyProviderHTTPS(r))

// Create a new CSR
const params = {
altNames: [commonName],
commonName: commonName,
// state: "WA",
// country: "US",
// organizationUnit: "NGINX",
emailAddress: email,
altNames: serverNames.length > 1 ? serverNames.slice(1) : [],
commonName: commonName,
emailAddress: email,
}

const result = await createCsr(params);
const pemExported = toPEM(result.pkcs10Ber, "CERTIFICATE REQUEST");

r.log(`njs-acme: [auto] Issuing a new Certificate: ${JSON.stringify(params)}`);

const prefix = r.variables.njs_acme_dir || NJS_ACME_DIR;

const privKey = await crypto.subtle.exportKey("pkcs8", result.keys.privateKey);
const pkeyPath = prefix + commonName + KEY_SUFFIX;
const pkeyPem = toPEM(privKey, "PRIVATE KEY");
fs.writeFileSync(pkeyPath, pkeyPem, 'utf-8');
r.log(`njs-acme: [auto] Wrote Private key to ${pkeyPath}`);

const challengePath = r.variables.njs_acme_challenge_dir!;
if (challengePath === undefined || challengePath.length == 0) {
r.return(500,"Nginx variable 'njs_acme_challenge_dir' must be set");
const result = await createCsr(params)
fs.writeFileSync(csrPath, toPEM(result.pkcs10Ber, 'CERTIFICATE REQUEST'))

const privKey = (await crypto.subtle.exportKey(
'pkcs8',
result.keys.privateKey
)) as ArrayBuffer
pkeyPem = toPEM(privKey, 'PRIVATE KEY')
fs.writeFileSync(pkeyPath, pkeyPem)
log.info(`Wrote Private key to ${pkeyPath}`)

// this is the only variable that has to be set in nginx.conf
const challengePath = r.variables.njs_acme_challenge_dir

if (challengePath === undefined || challengePath.length === 0) {
return r.return(
500,
"Nginx variable 'njs_acme_challenge_dir' must be set"
)
}
log.info('Issuing a new Certificate:', params)
const fullChallengePath = joinPaths(
challengePath,
'.well-known/acme-challenge'
)
try {
fs.mkdirSync(fullChallengePath, { recursive: true })
} catch (e) {
log.error(
`Error creating directory to store challenges at ${fullChallengePath}. Ensure the ${challengePath} directory is writable by the nginx user.`
)

return r.return(500, 'Cannot create challenge directory')
}
const certificatePem = await client.auto({
csr: result.pkcs10Ber,
email: email,
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
ngx.log(ngx.INFO, `njs-acme: [auto] Challenge Create (authz='${JSON.stringify(authz)}', challenge='${JSON.stringify(challenge)}', keyAuthorization='${keyAuthorization}')`);
ngx.log(ngx.INFO, `njs-acme: [auto] Writing challenge file so nginx can serve it via .well-known/acme-challenge/${challenge.token}`);
const path = `${challengePath}/.well-known/acme-challenge/${challenge.token}`;
fs.writeFileSync(path, keyAuthorization, 'utf8');
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const path = `${challengePath}/.well-known/acme-challenge/${challenge.token}`;
try {
fs.unlinkSync(path);
ngx.log(ngx.INFO, `njs-acme: [auto] removed challenge ${path}`);
} catch (e) {
ngx.log(ngx.ERR, `njs-acme: [auto] failed to remove challenge ${path}`);
}

certificatePem = await client.auto({
csr: Buffer.from(result.pkcs10Ber),
email: email,
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
log.info('Challenge Create', { authz, challenge, keyAuthorization })
log.info(
`Writing challenge file so nginx can serve it via .well-known/acme-challenge/${challenge.token}`
)

const path = joinPaths(fullChallengePath, challenge.token)
fs.writeFileSync(path, keyAuthorization)
},
challengeRemoveFn: async (_authz, challenge, _keyAuthorization) => {
const path = joinPaths(fullChallengePath, challenge.token)
try {
fs.unlinkSync(path)
log.info(`removed challenge ${path}`)
} catch (e) {
log.error(`failed to remove challenge ${path}`)
}
});
},
})
certInfo = await readCertificateInfo(certificatePem)
fs.writeFileSync(certPath, certificatePem)
log.info(`wrote certificate to ${certPath}`)
}

const certPath = prefix + commonName + CERTIFICATE_SUFFIX;
fs.writeFileSync(certPath, certificatePem, 'utf-8');
r.log(`njs-acme: wrote certificate to ${certPath}`);
const info = {
certificate: certInfo,
renewedCertificate: renewCertificate,
}

const info = {
certificate: certificatePem,
certificateKey: pkeyPem,
csr: pemExported
}
return r.return(200, JSON.stringify(info));
return r.return(200, JSON.stringify(info))
}
```

Expand Down
Loading

0 comments on commit a9342d8

Please sign in to comment.