mirror of https://github.com/requarks/wiki
parent
73da73a595
commit
c6933a2d20
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in new issue