mirror of https://github.com/Requarks/wiki.git
13 changed files with 458 additions and 159 deletions
Unified View
Diff Options
-
55client/components/admin/admin-pages-visualize.vue
-
11config.sample.yml
-
6dev/build/Dockerfile
-
7dev/build/config.yml
-
1dev/examples/docker-compose.yml
-
4dev/index.js
-
5package.json
-
25server/controllers/letsencrypt.js
-
1server/core/kernel.js
-
125server/core/letsencrypt.js
-
159server/core/servers.js
-
142server/master.js
-
76yarn.lock
@ -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 |
||||
|
} |
||||
|
} |
xxxxxxxxxx