You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
5.2 KiB

  1. const ACME = require('acme')
  2. const Keypairs = require('@root/keypairs')
  3. const _ = require('lodash')
  4. const moment = require('moment')
  5. const CSR = require('@root/csr')
  6. const PEM = require('@root/pem')
  7. // eslint-disable-next-line node/no-deprecated-api
  8. const punycode = require('punycode')
  9. /* global WIKI */
  10. module.exports = {
  11. apiDirectory: WIKI.dev ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory',
  12. acme: null,
  13. async init () {
  14. if (!_.get(WIKI.config, 'letsencrypt.payload', false)) {
  15. await this.requestCertificate()
  16. } else if (WIKI.config.letsencrypt.domain !== WIKI.config.ssl.domain) {
  17. WIKI.logger.info(`(LETSENCRYPT) Domain has changed. Requesting new certificates...`)
  18. await this.requestCertificate()
  19. } else if (moment(WIKI.config.letsencrypt.payload.expires).isSameOrBefore(moment().add(5, 'days'))) {
  20. WIKI.logger.info(`(LETSENCRYPT) Certificate is about to or has expired, requesting a new one...`)
  21. await this.requestCertificate()
  22. } else {
  23. WIKI.logger.info(`(LETSENCRYPT) Using existing certificate for ${WIKI.config.ssl.domain}, expires on ${WIKI.config.letsencrypt.payload.expires}: [ OK ]`)
  24. }
  25. WIKI.config.ssl.format = 'pem'
  26. WIKI.config.ssl.inline = true
  27. WIKI.config.ssl.key = WIKI.config.letsencrypt.serverKey
  28. WIKI.config.ssl.cert = WIKI.config.letsencrypt.payload.cert + '\n' + WIKI.config.letsencrypt.payload.chain
  29. WIKI.config.ssl.passphrase = null
  30. WIKI.config.ssl.dhparam = null
  31. },
  32. async requestCertificate () {
  33. try {
  34. WIKI.logger.info(`(LETSENCRYPT) Initializing Let's Encrypt client...`)
  35. this.acme = ACME.create({
  36. maintainerEmail: WIKI.config.maintainerEmail,
  37. packageAgent: `wikijs/${WIKI.version}`,
  38. notify: (ev, msg) => {
  39. if (_.includes(['warning', 'error'], ev)) {
  40. WIKI.logger.warn(`${ev}: ${msg}`)
  41. } else {
  42. WIKI.logger.debug(`${ev}: ${JSON.stringify(msg)}`)
  43. }
  44. }
  45. })
  46. await this.acme.init(this.apiDirectory)
  47. // -> Create ACME Subscriber account
  48. if (!_.get(WIKI.config, 'letsencrypt.account', false)) {
  49. WIKI.logger.info(`(LETSENCRYPT) Setting up account for the first time...`)
  50. const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' })
  51. const account = await this.acme.accounts.create({
  52. subscriberEmail: WIKI.config.ssl.subscriberEmail,
  53. agreeToTerms: true,
  54. accountKey: accountKeypair.private
  55. })
  56. WIKI.config.letsencrypt = {
  57. accountKeypair: accountKeypair,
  58. account: account,
  59. domain: WIKI.config.ssl.domain
  60. }
  61. await WIKI.configSvc.saveToDb(['letsencrypt'])
  62. WIKI.logger.info(`(LETSENCRYPT) Account was setup successfully [ OK ]`)
  63. }
  64. // -> Create Server Keypair
  65. if (!WIKI.config.letsencrypt.serverKey) {
  66. WIKI.logger.info(`(LETSENCRYPT) Generating server keypairs...`)
  67. const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' })
  68. WIKI.config.letsencrypt.serverKey = await Keypairs.export({ jwk: serverKeypair.private })
  69. WIKI.logger.info(`(LETSENCRYPT) Server keypairs generated successfully [ OK ]`)
  70. }
  71. // -> Create CSR
  72. WIKI.logger.info(`(LETSENCRYPT) Generating certificate signing request (CSR)...`)
  73. const domains = [ punycode.toASCII(WIKI.config.ssl.domain) ]
  74. const serverKey = await Keypairs.import({ pem: WIKI.config.letsencrypt.serverKey })
  75. const csrDer = await CSR.csr({ jwk: serverKey, domains, encoding: 'der' })
  76. const csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: csrDer })
  77. WIKI.logger.info(`(LETSENCRYPT) CSR generated successfully [ OK ]`)
  78. // -> Verify Domain + Get Certificate
  79. WIKI.logger.info(`(LETSENCRYPT) Requesting certificate from Let's Encrypt...`)
  80. const certResp = await this.acme.certificates.create({
  81. account: WIKI.config.letsencrypt.account,
  82. accountKey: WIKI.config.letsencrypt.accountKeypair.private,
  83. csr,
  84. domains,
  85. challenges: {
  86. 'http-01': {
  87. init () {},
  88. set (data) {
  89. WIKI.logger.info(`(LETSENCRYPT) Setting HTTP challenge for ${data.challenge.hostname}: [ READY ]`)
  90. WIKI.config.letsencrypt.challenge = data.challenge
  91. WIKI.logger.info(`(LETSENCRYPT) Waiting for challenge to complete...`)
  92. return null // <- this is needed, cannot be undefined
  93. },
  94. get (data) {
  95. return WIKI.config.letsencrypt.challenge
  96. },
  97. async remove (data) {
  98. WIKI.logger.info(`(LETSENCRYPT) Removing HTTP challenge: [ OK ]`)
  99. WIKI.config.letsencrypt.challenge = null
  100. return null // <- this is needed, cannot be undefined
  101. }
  102. }
  103. }
  104. })
  105. WIKI.logger.info(`(LETSENCRYPT) New certifiate received successfully: [ COMPLETED ]`)
  106. WIKI.config.letsencrypt.payload = certResp
  107. WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain
  108. await WIKI.configSvc.saveToDb(['letsencrypt'])
  109. } catch (err) {
  110. WIKI.logger.warn(`(LETSENCRYPT) ${err}`)
  111. throw err
  112. }
  113. }
  114. }