feat: let's encrypt

This commit is contained in:
NGPixel
2020-01-11 22:33:19 -05:00
parent 73da73a595
commit c6933a2d20
13 changed files with 459 additions and 160 deletions

View File

@@ -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

View File

@@ -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
server/core/letsencrypt.js Normal file
View File

@@ -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
server/core/servers.js Normal file
View File

@@ -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
}
}

View File

@@ -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 = {}
await WIKI.servers.startGraphQL()
await WIKI.servers.startHTTP()
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 ]')
}
})
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