Skip to content

Commit

Permalink
feat: let's encrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
NGPixel committed Jan 12, 2020
1 parent 73da73a commit c6933a2
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 159 deletions.
55 changes: 38 additions & 17 deletions client/components/admin/admin-pages-visualize.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 => {
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -180,6 +191,9 @@ export default {
this.$refs.svgContainer.appendChild(svg.node())
},
/**
* Hierarchical Tree
*/
drawTree () {
const data = this.hierarchy(this.pages, true)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
11 changes: 8 additions & 3 deletions config.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion dev/build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
7 changes: 6 additions & 1 deletion dev/build/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions dev/examples/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
restart: unless-stopped
ports:
- "80:3000"
- "443:3443"

volumes:
db-data:
4 changes: 2 additions & 2 deletions dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...'))

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions server/controllers/letsencrypt.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions server/core/kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
125 changes: 125 additions & 0 deletions server/core/letsencrypt.js
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading

0 comments on commit c6933a2

Please sign in to comment.