瀏覽代碼

feat: let's encrypt

NGPixel 5 年之前
父節點
當前提交
c6933a2d20

+ 38 - 17
client/components/admin/admin-pages-visualize.vue

@@ -29,7 +29,6 @@
             v-btn.px-5(value='rradial')
               v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
               span.text-none Relational Radial
-              v-chip.ml-3(x-small) Beta
         .admin-pages-visualize-svg.pa-10(ref='svgContainer')
         v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
 </template>
@@ -61,9 +60,14 @@ export default {
     }
   },
   methods: {
+    goToPage (d) {
+      if (_.get(d, 'data.id', 0) > 0) {
+        this.$router.push(`${d.data.id}`)
+      }
+    },
     bilink (root) {
-      const map = new Map(root.leaves().map(d => [d.data.path, d]))
-      for (const d of root.leaves()) {
+      const map = new Map(root.descendants().map(d => [d.data.path, d]))
+      for (const d of root.descendants()) {
         d.incoming = []
         d.outgoing = []
         d.data.links.forEach(i => {
@@ -73,7 +77,7 @@ export default {
           }
         })
       }
-      for (const d of root.leaves()) {
+      for (const d of root.descendants()) {
         for (const o of d.outgoing) {
           if (o[1]) {
             o[1].incoming.push(o)
@@ -112,8 +116,11 @@ export default {
         children: result
       }
     },
+    /**
+     * Relational Radial
+     */
     drawRelations () {
-      const data = this.hierarchy(this.pages)
+      const data = this.hierarchy(this.pages, true)
 
       const line = d3.lineRadial()
         .curve(d3.curveBundle.beta(0.85))
@@ -124,16 +131,26 @@ export default {
         .size([2 * Math.PI, this.radius - 100])
 
       const root = tree(this.bilink(d3.hierarchy(data)
-        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.title, b.data.title))))
+        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))
 
       const svg = d3.create('svg')
         .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
 
+      const link = svg.append('g')
+        .attr('stroke', '#CCC')
+        .attr('fill', 'none')
+        .selectAll('path')
+        .data(root.descendants().flatMap(leaf => leaf.outgoing))
+        .join('path')
+        .style('mix-blend-mode', 'multiply')
+        .attr('d', ([i, o]) => line(i.path(o)))
+        .each(function(d) { d.path = this })
+
       svg.append('g')
         .attr('font-family', 'sans-serif')
         .attr('font-size', 10)
         .selectAll('g')
-        .data(root.leaves())
+        .data(root.descendants())
         .join('g')
         .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
         .append('text')
@@ -142,23 +159,17 @@ export default {
         .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
         .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
+        .attr('cursor', 'pointer')
         .text(d => d.data.title)
         .each(function(d) { d.text = this })
         .on('mouseover', overed)
         .on('mouseout', outed)
+        .on('click', d => this.goToPage(d))
         .call(text => text.append('title').text(d => `${d.data.path}
           ${d.outgoing.length} outgoing
           ${d.incoming.length} incoming`))
-
-      const link = svg.append('g')
-        .attr('stroke', '#CCC')
-        .attr('fill', 'none')
-        .selectAll('path')
-        .data(root.leaves().flatMap(leaf => leaf.outgoing))
-        .join('path')
-        .style('mix-blend-mode', 'multiply')
-        .attr('d', ([i, o]) => line(i.path(o)))
-        .each(function(d) { d.path = this })
+        .clone(true).lower()
+        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 
       function overed(d) {
         link.style('mix-blend-mode', null)
@@ -180,6 +191,9 @@ export default {
 
       this.$refs.svgContainer.appendChild(svg.node())
     },
+    /**
+     * Hierarchical Tree
+     */
     drawTree () {
       const data = this.hierarchy(this.pages, true)
 
@@ -232,12 +246,17 @@ export default {
         .attr('x', d => d.children ? -6 : 6)
         .attr('text-anchor', d => d.children ? 'end' : 'start')
         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
+        .attr('cursor', 'pointer')
         .text(d => d.data.title)
+        .on('click', d => this.goToPage(d))
         .clone(true).lower()
         .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 
       this.$refs.svgContainer.appendChild(svg.node())
     },
+    /**
+     * Hierarchical Radial
+     */
     drawRadialTree () {
       const data = this.hierarchy(this.pages)
 
@@ -286,7 +305,9 @@ export default {
         .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
         /* eslint-enable no-mixed-operators */
         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
+        .attr('cursor', 'pointer')
         .text(d => d.data.title)
+        .on('click', d => this.goToPage(d))
         .clone(true).lower()
         .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 

+ 8 - 3
config.sample.yml

@@ -59,7 +59,12 @@ db:
 
 ssl:
   enabled: false
+  port: 3443
 
+  # Provider to use, possible values: custom, letsencrypt
+  provider: custom
+
+  # ++++++ For custom only ++++++
   # Certificate format, either 'pem' or 'pfx':
   format: pem
   # Using PEM format:
@@ -73,9 +78,9 @@ ssl:
   # to 1024 bits (default: null):
   dhparam: null
 
-  # Listen on this HTTP port and redirect all requests to HTTPS.
-  # Set to false to disable (default: 80):
-  redirectNonSSLPort: 80
+  # ++++++ For letsencrypt only ++++++
+  domain: wiki.yourdomain.com
+  maintainerEmail: admin@example.com
 
 # ---------------------------------------------------------------------
 # Database Pool Options

+ 5 - 1
dev/build/Dockerfile

@@ -29,6 +29,7 @@ LABEL maintainer="requarks.io"
 RUN apk add bash curl git openssh gnupg sqlite --no-cache && \
     mkdir -p /wiki && \
     mkdir -p /logs && \
+    mkdir -p /wiki/data/content && \
     chown -R node:node /wiki /logs
 
 WORKDIR /wiki
@@ -44,8 +45,11 @@ COPY --chown=node:node ./LICENSE ./LICENSE
 
 USER node
 
+VOLUME ["/wiki/data/content"]
+
 EXPOSE 3000
+EXPOSE 3443
 
-# HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://localhost/healthz
+# HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://localhost:3000/healthz
 
 CMD ["node", "server"]

+ 6 - 1
dev/build/config.yml

@@ -9,5 +9,10 @@ db:
   db: $(DB_NAME)
   storage: $(DB_FILEPATH)
   ssl: $(DB_SSL)
-trustProxy: $(TRUST_PROXY)
+ssl:
+  enabled: $(SSL_ACTIVE)
+  port: 3443
+  provider: letsencrypt
+  domain: $(LETSENCRYPT_DOMAIN)
+  maintainerEmail: $(LETSENCRYPT_EMAIL)
 logLevel: info

+ 1 - 0
dev/examples/docker-compose.yml

@@ -27,6 +27,7 @@ services:
     restart: unless-stopped
     ports:
       - "80:3000"
+      - "443:3443"
 
 volumes:
   db-data:

+ 2 - 2
dev/index.js

@@ -67,8 +67,8 @@ const init = {
     console.warn(chalk.yellow('--- Closing DB connections...'))
     await global.WIKI.models.knex.destroy()
     console.warn(chalk.yellow('--- Closing Server connections...'))
-    if (global.WIKI.server) {
-      await new Promise((resolve, reject) => global.WIKI.server.destroy(resolve))
+    if (global.WIKI.servers) {
+      await global.WIKI.servers.stopServers()
     }
     console.warn(chalk.yellow('--- Purging node modules cache...'))
 

+ 5 - 0
package.json

@@ -39,6 +39,10 @@
     "@azure/storage-blob": "12.0.1",
     "@bugsnag/js": "6.5.0",
     "@exlinc/keycloak-passport": "1.0.2",
+    "@root/csr": "0.8.1",
+    "@root/keypairs": "0.9.0",
+    "@root/pem": "1.0.4",
+    "acme": "3.0.3",
     "algoliasearch": "3.35.1",
     "apollo-fetch": "0.7.0",
     "apollo-server": "2.9.15",
@@ -143,6 +147,7 @@
     "pg-query-stream": "2.1.2",
     "pg-tsquery": "8.1.0",
     "pug": "2.0.4",
+    "punycode": "2.1.1",
     "qr-image": "3.2.0",
     "raven": "2.6.4",
     "remove-markdown": "0.3.0",

+ 25 - 0
server/controllers/letsencrypt.js

@@ -0,0 +1,25 @@
+const express = require('express')
+const router = express.Router()
+const _ = require('lodash')
+
+/* global WIKI */
+
+/**
+ * Let's Encrypt Challenge
+ */
+router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
+  res.type('text/plain')
+  if (_.get(WIKI.config, 'letsencrypt.challenge', false)) {
+    if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
+      res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
+      WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
+    } else {
+      res.status(406).send('Invalid Challenge Token!')
+      WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
+    }
+  } else {
+    res.status(418).end()
+  }
+})
+
+module.exports = router

+ 1 - 0
server/core/kernel.js

@@ -34,6 +34,7 @@ module.exports = {
       await this.initTelemetry()
       WIKI.cache = require('./cache').init()
       WIKI.scheduler = require('./scheduler').init()
+      WIKI.servers = require('./servers')
       WIKI.sideloader = require('./sideloader').init()
       WIKI.events = new EventEmitter()
     } catch (err) {

+ 125 - 0
server/core/letsencrypt.js

@@ -0,0 +1,125 @@
+const ACME = require('acme')
+const Keypairs = require('@root/keypairs')
+const _ = require('lodash')
+const moment = require('moment')
+const CSR = require('@root/csr')
+const PEM = require('@root/pem')
+// eslint-disable-next-line node/no-deprecated-api
+const punycode = require('punycode')
+
+/* global WIKI */
+
+module.exports = {
+  apiDirectory: WIKI.dev ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory',
+  acme: null,
+  async init () {
+    if (!_.get(WIKI.config, 'letsencrypt.payload', false)) {
+      await this.requestCertificate()
+    } else if (WIKI.config.letsencrypt.domain !== WIKI.config.ssl.domain) {
+      WIKI.logger.info(`(LETSENCRYPT) Domain has changed. Requesting new certificates...`)
+      await this.requestCertificate()
+    } else if (moment(WIKI.config.letsencrypt.payload.expires).isSameOrBefore(moment().add(5, 'days'))) {
+      WIKI.logger.info(`(LETSENCRYPT) Certificate is about to or has expired, requesting a new one...`)
+      await this.requestCertificate()
+    } else {
+      WIKI.logger.info(`(LETSENCRYPT) Using existing certificate for ${WIKI.config.ssl.domain}, expires on ${WIKI.config.letsencrypt.payload.expires}: [ OK ]`)
+    }
+    WIKI.config.ssl.format = 'pem'
+    WIKI.config.ssl.inline = true
+    WIKI.config.ssl.key = WIKI.config.letsencrypt.serverKey
+    WIKI.config.ssl.cert = WIKI.config.letsencrypt.payload.cert + '\n' + WIKI.config.letsencrypt.payload.chain
+    WIKI.config.ssl.passphrase = null
+    WIKI.config.ssl.dhparam = null
+  },
+  async requestCertificate () {
+    try {
+      WIKI.logger.info(`(LETSENCRYPT) Initializing Let's Encrypt client...`)
+      this.acme = ACME.create({
+        maintainerEmail: WIKI.config.ssl.maintainerEmail,
+        packageAgent: `wikijs/${WIKI.version}`,
+        notify: (ev, msg) => {
+          if (_.includes(['warning', 'error'], ev)) {
+            WIKI.logger.warn(`${ev}: ${msg}`)
+          } else {
+            WIKI.logger.debug(`${ev}: ${JSON.stringify(msg)}`)
+          }
+        }
+      })
+
+      await this.acme.init(this.apiDirectory)
+
+      // -> Create ACME Subscriber account
+
+      if (!_.get(WIKI.config, 'letsencrypt.account', false)) {
+        WIKI.logger.info(`(LETSENCRYPT) Setting up account for the first time...`)
+        const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' })
+        const account = await this.acme.accounts.create({
+          subscriberEmail: WIKI.config.ssl.maintainerEmail,
+          agreeToTerms: true,
+          accountKey: accountKeypair.private
+        })
+        WIKI.config.letsencrypt = {
+          accountKeypair: accountKeypair,
+          account: account,
+          domain: WIKI.config.ssl.domain
+        }
+        await WIKI.configSvc.saveToDb(['letsencrypt'])
+        WIKI.logger.info(`(LETSENCRYPT) Account was setup successfully [ OK ]`)
+      }
+
+      // -> Create Server Keypair
+
+      if (!WIKI.config.letsencrypt.serverKey) {
+        WIKI.logger.info(`(LETSENCRYPT) Generating server keypairs...`)
+        const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' })
+        WIKI.config.letsencrypt.serverKey = await Keypairs.export({ jwk: serverKeypair.private })
+        WIKI.logger.info(`(LETSENCRYPT) Server keypairs generated successfully [ OK ]`)
+      }
+
+      // -> Create CSR
+
+      WIKI.logger.info(`(LETSENCRYPT) Generating certificate signing request (CSR)...`)
+      const domains = [ punycode.toASCII(WIKI.config.ssl.domain) ]
+      const serverKey = await Keypairs.import({ pem: WIKI.config.letsencrypt.serverKey })
+      const csrDer = await CSR.csr({ jwk: serverKey, domains, encoding: 'der' })
+      const csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: csrDer })
+      WIKI.logger.info(`(LETSENCRYPT) CSR generated successfully [ OK ]`)
+
+      // -> Verify Domain + Get Certificate
+
+      WIKI.logger.info(`(LETSENCRYPT) Requesting certificate from Let's Encrypt...`)
+      const certResp = await this.acme.certificates.create({
+        account: WIKI.config.letsencrypt.account,
+        accountKey: WIKI.config.letsencrypt.accountKeypair.private,
+        csr,
+        domains,
+        challenges: {
+          'http-01': {
+            init () {},
+            set (data) {
+              WIKI.logger.info(`(LETSENCRYPT) Setting HTTP challenge for ${data.challenge.hostname}: [ READY ]`)
+              WIKI.config.letsencrypt.challenge = data.challenge
+              WIKI.logger.info(`(LETSENCRYPT) Waiting for challenge to complete...`)
+              return null // <- this is needed, cannot be undefined
+            },
+            get (data) {
+              return WIKI.config.letsencrypt.challenge
+            },
+            async remove (data) {
+              WIKI.logger.info(`(LETSENCRYPT) Removing HTTP challenge: [ OK ]`)
+              WIKI.config.letsencrypt.challenge = null
+              return null // <- this is needed, cannot be undefined
+            }
+          }
+        }
+      })
+      WIKI.logger.info(`(LETSENCRYPT) New certifiate received successfully: [ COMPLETED ]`)
+      WIKI.config.letsencrypt.payload = certResp
+      WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain
+      await WIKI.configSvc.saveToDb(['letsencrypt'])
+    } catch (err) {
+      WIKI.logger.warn(`(LETSENCRYPT) ${err}`)
+      throw err
+    }
+  }
+}

+ 159 - 0
server/core/servers.js

@@ -0,0 +1,159 @@
+const fs = require('fs-extra')
+const http = require('http')
+const https = require('https')
+const { ApolloServer } = require('apollo-server-express')
+const Promise = require('bluebird')
+const _ = require('lodash')
+
+/* global WIKI */
+
+module.exports = {
+  servers: {
+    graph: null,
+    http: null,
+    https: null
+  },
+  connections: new Map(),
+  le: null,
+  /**
+   * Start HTTP Server
+   */
+  async startHTTP () {
+    WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
+    this.servers.http = http.createServer(WIKI.app)
+    this.servers.graph.installSubscriptionHandlers(this.servers.http)
+
+    this.servers.http.listen(WIKI.config.port, WIKI.config.bindIP)
+    this.servers.http.on('error', (error) => {
+      if (error.syscall !== 'listen') {
+        throw error
+      }
+
+      switch (error.code) {
+        case 'EACCES':
+          WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
+          return process.exit(1)
+        case 'EADDRINUSE':
+          WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
+          return process.exit(1)
+        default:
+          throw error
+      }
+    })
+
+    this.servers.http.on('listening', () => {
+      WIKI.logger.info('HTTP Server: [ RUNNING ]')
+    })
+
+    this.servers.http.on('connection', conn => {
+      let connKey = `${conn.remoteAddress}:${conn.remotePort}`
+      this.connections.set(connKey, conn)
+      conn.on('close', () => {
+        this.connections.delete(connKey)
+      })
+    })
+  },
+  /**
+   * Start HTTPS Server
+   */
+  async startHTTPS () {
+    if (WIKI.config.ssl.provider === 'letsencrypt') {
+      this.le = require('./letsencrypt')
+      await this.le.init()
+    }
+
+    WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.ssl.port} ]`)
+    const tlsOpts = {}
+    try {
+      if (WIKI.config.ssl.format === 'pem') {
+        tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : fs.readFileSync(WIKI.config.ssl.key)
+        tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : fs.readFileSync(WIKI.config.ssl.cert)
+      } else {
+        tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : fs.readFileSync(WIKI.config.ssl.pfx)
+      }
+      if (!_.isEmpty(WIKI.config.ssl.passphrase)) {
+        tlsOpts.passphrase = WIKI.config.ssl.passphrase
+      }
+      if (!_.isEmpty(WIKI.config.ssl.dhparam)) {
+        tlsOpts.dhparam = WIKI.config.ssl.dhparam
+      }
+    } catch (err) {
+      WIKI.logger.error('Failed to setup HTTPS server parameters:')
+      WIKI.logger.error(err)
+      return process.exit(1)
+    }
+    this.servers.https = https.createServer(tlsOpts, WIKI.app)
+    this.servers.graph.installSubscriptionHandlers(this.servers.https)
+
+    this.servers.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)
+    this.servers.https.on('error', (error) => {
+      if (error.syscall !== 'listen') {
+        throw error
+      }
+
+      switch (error.code) {
+        case 'EACCES':
+          WIKI.logger.error('Listening on port ' + WIKI.config.ssl.port + ' requires elevated privileges!')
+          return process.exit(1)
+        case 'EADDRINUSE':
+          WIKI.logger.error('Port ' + WIKI.config.ssl.port + ' is already in use!')
+          return process.exit(1)
+        default:
+          throw error
+      }
+    })
+
+    this.servers.https.on('listening', () => {
+      WIKI.logger.info('HTTPS Server: [ RUNNING ]')
+    })
+
+    this.servers.https.on('connection', conn => {
+      let connKey = `${conn.remoteAddress}:${conn.remotePort}`
+      this.connections.set(connKey, conn)
+      conn.on('close', () => {
+        this.connections.delete(connKey)
+      })
+    })
+  },
+  /**
+   * Start GraphQL Server
+   */
+  async startGraphQL () {
+    const graphqlSchema = require('../graph')
+    this.servers.graph = new ApolloServer({
+      ...graphqlSchema,
+      context: ({ req, res }) => ({ req, res }),
+      subscriptions: {
+        onConnect: (connectionParams, webSocket) => {
+
+        },
+        path: '/graphql-subscriptions'
+      }
+    })
+    this.servers.graph.applyMiddleware({ app: WIKI.app })
+  },
+  /**
+   * Close all active connections
+   */
+  closeConnections () {
+    for (const conn of this.connections) {
+      conn.destroy()
+    }
+    this.connections.clear()
+  },
+  /**
+   * Stop all servers
+   */
+  async stopServers () {
+    this.closeConnections()
+    if (this.servers.http) {
+      await Promise.fromCallback(cb => { this.servers.http.close(cb) })
+      this.servers.http = null
+    }
+    if (this.servers.https) {
+      await Promise.fromCallback(cb => { this.servers.https.close(cb) })
+      this.servers.https = null
+    }
+    this.servers.graph = null
+  }
+}

+ 12 - 130
server/master.js

@@ -7,12 +7,8 @@ const express = require('express')
 const session = require('express-session')
 const KnexSessionStore = require('connect-session-knex')(session)
 const favicon = require('serve-favicon')
-const fs = require('fs-extra')
-const http = require('http')
-const https = require('https')
 const path = require('path')
 const _ = require('lodash')
-const { ApolloServer } = require('apollo-server-express')
 
 /* global WIKI */
 
@@ -62,6 +58,12 @@ module.exports = async () => {
     maxAge: '7d'
   }))
 
+  // ----------------------------------------
+  // Let's Encrypt Challenge
+  // ----------------------------------------
+
+  app.use('/', ctrl.letsencrypt)
+
   // ----------------------------------------
   // Passport Authentication
   // ----------------------------------------
@@ -104,6 +106,7 @@ module.exports = async () => {
   // View accessible data
   // ----------------------------------------
 
+  app.locals.siteConfig = {}
   app.locals.analyticsCode = {}
   app.locals.basedir = WIKI.ROOTPATH
   app.locals.config = WIKI.config
@@ -124,23 +127,6 @@ module.exports = async () => {
     app.use(global.WP_DEV.hotMiddleware)
   }
 
-  // ----------------------------------------
-  // Apollo Server (GraphQL)
-  // ----------------------------------------
-
-  const graphqlSchema = require('./graph')
-  const apolloServer = new ApolloServer({
-    ...graphqlSchema,
-    context: ({ req, res }) => ({ req, res }),
-    subscriptions: {
-      onConnect: (connectionParams, webSocket) => {
-
-      },
-      path: '/graphql-subscriptions'
-    }
-  })
-  apolloServer.applyMiddleware({ app })
-
   // ----------------------------------------
   // Routing
   // ----------------------------------------
@@ -184,118 +170,14 @@ module.exports = async () => {
   })
 
   // ----------------------------------------
-  // HTTP/S server
+  // Start HTTP Server(s)
   // ----------------------------------------
 
-  let srvConnections = {}
-
-  app.set('port', WIKI.config.port)
-  if (WIKI.config.ssl.enabled) {
-    WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.port} ]`)
-    const tlsOpts = {}
-    try {
-      if (WIKI.config.ssl.format === 'pem') {
-        tlsOpts.key = fs.readFileSync(WIKI.config.ssl.key)
-        tlsOpts.cert = fs.readFileSync(WIKI.config.ssl.cert)
-      } else {
-        tlsOpts.pfx = fs.readFileSync(WIKI.config.ssl.pfx)
-      }
-      if (!_.isEmpty(WIKI.config.ssl.passphrase)) {
-        tlsOpts.passphrase = WIKI.config.ssl.passphrase
-      }
-      if (!_.isEmpty(WIKI.config.ssl.dhparam)) {
-        tlsOpts.dhparam = WIKI.config.ssl.dhparam
-      }
-    } catch (err) {
-      WIKI.logger.error('Failed to setup HTTPS server parameters:')
-      WIKI.logger.error(err)
-      return process.exit(1)
-    }
-    WIKI.server = https.createServer(tlsOpts, app)
-
-    // HTTP Redirect Server
-    if (WIKI.config.ssl.redirectNonSSLPort) {
-      WIKI.serverAlt = http.createServer((req, res) => {
-        res.writeHead(301, { 'Location': 'https://' + req.headers['host'] + req.url })
-        res.end()
-      })
-    }
-  } else {
-    WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
-    WIKI.server = http.createServer(app)
-  }
-  apolloServer.installSubscriptionHandlers(WIKI.server)
-
-  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
-  WIKI.server.on('error', (error) => {
-    if (error.syscall !== 'listen') {
-      throw error
-    }
-
-    // handle specific listen errors with friendly messages
-    switch (error.code) {
-      case 'EACCES':
-        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
-        return process.exit(1)
-      case 'EADDRINUSE':
-        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
-        return process.exit(1)
-      default:
-        throw error
-    }
-  })
-
-  WIKI.server.on('connection', conn => {
-    let key = `${conn.remoteAddress}:${conn.remotePort}`
-    srvConnections[key] = conn
-    conn.on('close', function() {
-      delete srvConnections[key]
-    })
-  })
-
-  WIKI.server.on('listening', () => {
-    if (WIKI.config.ssl.enabled) {
-      WIKI.logger.info('HTTPS Server: [ RUNNING ]')
-
-      // Start HTTP Redirect Server
-      if (WIKI.config.ssl.redirectNonSSLPort) {
-        WIKI.serverAlt.listen(WIKI.config.ssl.redirectNonSSLPort, WIKI.config.bindIP)
-
-        WIKI.serverAlt.on('error', (error) => {
-          if (error.syscall !== 'listen') {
-            throw error
-          }
-
-          switch (error.code) {
-            case 'EACCES':
-              WIKI.logger.error('(HTTP Redirect) Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
-              return process.exit(1)
-            case 'EADDRINUSE':
-              WIKI.logger.error('(HTTP Redirect) Port ' + WIKI.config.port + ' is already in use!')
-              return process.exit(1)
-            default:
-              throw error
-          }
-        })
-
-        WIKI.serverAlt.on('listening', () => {
-          WIKI.logger.info('HTTP Server: [ RUNNING in redirect mode ]')
-        })
-      }
-    } else {
-      WIKI.logger.info('HTTP Server: [ RUNNING ]')
-    }
-  })
+  await WIKI.servers.startGraphQL()
+  await WIKI.servers.startHTTP()
 
-  WIKI.server.destroy = (cb) => {
-    WIKI.server.close(cb)
-    for (let key in srvConnections) {
-      srvConnections[key].destroy()
-    }
-
-    if (WIKI.config.ssl.enabled && WIKI.config.ssl.redirectNonSSLPort) {
-      WIKI.serverAlt.close(cb)
-    }
+  if (WIKI.config.ssl.enabled === true || WIKI.config.ssl.enabled === 'true' || WIKI.config.ssl.enabled === 1 || WIKI.config.ssl.enabled === '1') {
+    await WIKI.servers.startHTTPS()
   }
 
   return true

+ 71 - 5
yarn.lock

@@ -1553,6 +1553,65 @@
   resolved "https://registry.yarnpkg.com/@requarks/ckeditor5/-/ckeditor5-12.4.0-wiki.14.tgz#eca91568cc5f1471fff684df18c638dfab9f0aef"
   integrity sha512-3DD2GmagyTYeSHsm437FR0OxlzNFYnc3PEflt7p0/mTM68nRHUHv/pO9wvbUXaRk667WKsRVqckbbMQbdRK43g==
 
+"@root/acme@^3.0.2":
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/@root/acme/-/acme-3.0.9.tgz#437f4b46015e70587fb50119807889c944370388"
+  integrity sha512-/FgJF6RUrkqNpLmxqjktHaWMsLOwma6D+e4EBoxKtTjTAI+dBqW8Z8cH38feUsiIBR5LimPeYmBo/oqU3oMkKQ==
+  dependencies:
+    "@root/encoding" "^1.0.1"
+    "@root/keypairs" "^0.9.0"
+    "@root/pem" "^1.0.4"
+    "@root/request" "^1.3.11"
+    "@root/x509" "^0.7.2"
+
+"@root/asn1@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@root/asn1/-/asn1-1.0.0.tgz#8748cf7b4497324de91a154b606ca1ddfe97c5d7"
+  integrity sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==
+  dependencies:
+    "@root/encoding" "^1.0.1"
+
+"@root/csr@0.8.1":
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/@root/csr/-/csr-0.8.1.tgz#97a1b821331a4ed5895eee33bedb6ad49cdef74e"
+  integrity sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==
+  dependencies:
+    "@root/asn1" "^1.0.0"
+    "@root/pem" "^1.0.4"
+    "@root/x509" "^0.7.2"
+
+"@root/encoding@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@root/encoding/-/encoding-1.0.1.tgz#60cf3ccb9e2239226b30e8394c7a4a258a161c3e"
+  integrity sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==
+
+"@root/keypairs@0.9.0", "@root/keypairs@^0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@root/keypairs/-/keypairs-0.9.0.tgz#0340a8a9cd865a9adc3ad5dd1f98221a7cb310df"
+  integrity sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==
+  dependencies:
+    "@root/encoding" "^1.0.1"
+    "@root/pem" "^1.0.4"
+    "@root/x509" "^0.7.2"
+
+"@root/pem@1.0.4", "@root/pem@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@root/pem/-/pem-1.0.4.tgz#9ecc3419c0a05c92b8ecbfe0a41f474970f8c72f"
+  integrity sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==
+
+"@root/request@^1.3.11":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@root/request/-/request-1.4.2.tgz#a51e93c64eb8b9b0df06f34677e63d8239c4311c"
+  integrity sha512-J8FM4+SJuc7WRC+Jz17m+VT2lgI7HtatHhxN1F2ck5aIKUAxJEaR4u/gLBsgT60mVHevKCjKN0O8115UtJjwLw==
+
+"@root/x509@^0.7.2":
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/@root/x509/-/x509-0.7.2.tgz#422ca7e46c09f5ddf3af86306383778eb004ace1"
+  integrity sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==
+  dependencies:
+    "@root/asn1" "^1.0.0"
+    "@root/encoding" "^1.0.1"
+
 "@types/accepts@*", "@types/accepts@^1.3.5":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
@@ -2232,6 +2291,13 @@ accepts@^1.3.5, accepts@~1.3.5, accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
+acme@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/acme/-/acme-3.0.3.tgz#e3dd9086027118d2cda7bf72572446c02e0696ea"
+  integrity sha512-NMEugs9cWvryNRidu9GeyV3EbqDlb5XFKQjaJ6E99RFpzrHM/iYYLImdrdpnkbS4vFTCrB2nMA7MQB0vAk801w==
+  dependencies:
+    "@root/acme" "^3.0.2"
+
 acorn-globals@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
@@ -12071,16 +12137,16 @@ punycode@1.3.2:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
   integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
 
+punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
 punycode@^1.2.4, punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
 
-punycode@^2.1.0, punycode@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
-  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-
 q@^1.1.2, q@^1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"