ahoy/index.js

146 lines
5.5 KiB
JavaScript

const bodyParser = require("body-parser")
const compare = require("safe-compare")
const crypto = require("crypto")
const fs = require("fs")
const express = require("express")
const humanizeDuration = require("humanize-duration")
const moment = require("moment")
const path = require("path")
const uuid = require("uuid")
// move old log
if (fs.existsSync(path.join(__dirname, "latest.log"))) {
if (!fs.existsSync(path.join(__dirname, "logs"))) fs.mkdirSync(path.join(__dirname, "logs"))
fs.renameSync(path.join(__dirname, "latest.log"), path.join(__dirname, "logs", `${new Date().toISOString()}.log`))
}
require("dotenv").config()
const deployer = require("./deployer")
const log = require("./logger")
const yeller = require("./yeller")
const sleep = ms => new Promise(res => setTimeout(res, ms))
if (!fs.existsSync(path.join(__dirname, ".env"))) {
log.error("No dotenv file specified")
process.exit(1)
}
humanize = (s) => humanizeDuration(s * 1000)
// Website
let app = express()
app.use(bodyParser.json({
verify: (req, res, buf) => { req.rawBody = buf }
}))
app.use((err, req, res, next) => {
if (req.path !== process.env.WEBHOOK_PAYLOAD) {
// Give false bad gateway
log.info(`Request received to path other than webhook main payload from ${req.socket.remoteAddress}`)
return res.status(DEFAULT_ERROR_RESPONSE)
}
next()
})
app.post(`/${process.env.WEBHOOK_PAYLOAD}`, async (req, res) => {
let started = moment().unix()
let wid = uuid.v4()
log.info(`[${wid}] Received deploy request from ${req.socket.remoteAddress}`)
// We need to verify that 4 headers exist:
let requirements = [
"x-github-delivery",
"x-github-event",
"x-hub-signature",
"x-hub-signature-256",
]
let count = 0
for (const requirement of requirements) {
if (req.headers.hasOwnProperty(requirement)) count += 1
}
if (count != requirements.length) {
log.error(`[${wid}] Failure - Request received, failed the required header check`)
return res.status(405)
}
// Verify content type
if (!compare(req.headers["content-type"], "application/json")) {
log.error(`[${wid}] Failure - Request received with wrong content type`)
return res.status(405)
}
// Now we have to verify that the Sha256 signature is correct.
// It is the HMAC hex digest of the request body and is generated using the webhook secret as the HMAC key.
let signature = crypto.createHmac("sha256", process.env.WEBHOOK_SECRET).update(req.rawBody).digest("hex")
signature = `sha256=${signature}`
if (!compare(signature, req.headers["x-hub-signature-256"])) {
log.error(`[${wid}] Failure - Event had invalid signature`)
return res.status(403)
} else {
log.info(`[${wid}] Good signature check. Recieved: "${req.headers["x-hub-signature-256"]}", ours: "${signature}"`)
}
// This is a legitimate request.
// Is the event a push?
if (req.headers["x-github-event"] != "push" || req.body.event != "push") {
log.error(`[${wid}] Failure - Event was not a push`)
return res.status(405)
}
log.info(`[${wid}] Deploying...`)
try {
if (fs.existsSync(path.join(__dirname, "DEPLOYING"))) {
log.warn(`[${wid}] Currently deploying, trying 10 times for 20 seconds each...`)
let count = 0
for (let i = 0; i < 10; i++) {
count += 1
log.info(`[${wid}] Attempt ${i} of 10...`)
await sleep(20000)
if (fs.existsSync(path.join(__dirname, "DEPLOYING")) && count >= 10) {
log.error(`[${wid}] Failed, timed out for deploy.`)
deployer.dump(wid).then((result) => {
res.status(500).send(result.dump + `\n\nA deploy was currently running which timed out.\n\n(took ${humanize(moment().unix() - started)})`)
yeller.error(wid, 1)
})
}
if (!fs.existsSync(path.join(__dirname, "DEPLOYING"))) break
}
}
deployer.deploy(wid).then((result) => {
log.info(`[${wid}] FINISHED - Deploy completed! (took ${humanize(moment().unix() - started)} seconds, started at unix timestamp ${started})`)
res.status(201).send(result.dump + `\n\nSuccessfully deployed!\n(build time: ${humanize(result.buildTime)}, mix time: ${humanize(result.mixTime)}, down time: ${humanize(result.downTime)}, total deploy time: ${humanize(moment().unix() - started)})`)
yeller.yell(wid, result.buildTime, result.downTime, moment().unix() - started, result.mixTime)
})
} catch (e) {
log.error(`[${wid}] EXCEPTION OCCURED on deploy!`)
log.error(`[${wid}] FAILURE REASON - '${e}'`)
log.error(`[${wid}] FAILURE STACK - ${e.stack}`)
deployer.dump(wid).then((result) => {
res.status(500).send(result.dump + `\n\nFailed to deploy.\n(took ${humanize(moment().unix() - started)})\n\nstack: ${e.stack}\nexception: ${e}`)
yeller.error(wid, 0, e)
})
}
})
// Start
app.listen(process.env.AHOY_PORT, () => {
log.info(`Ahoy running, listening for pushes on port ${process.env.AHOY_PORT}`)
})
yeller.login(process.env.YELLER_TOKEN, process.env.DEPLOY_LOG_CHANNEL_ID, process.env.DEPLOY_ERROR_CHANNEL_ID, process.env.DEVELOPER_ROLE_ID)
if (!fs.existsSync(path.join("/var", "www", "maintenance"))) fs.mkdirSync(path.join("/var", "www", "maintenance"))