Skip to content

Commit

Permalink
send webhooks & filter event/action & validate secret
Browse files Browse the repository at this point in the history
  • Loading branch information
DEVTomatoCake committed Nov 2, 2023
1 parent d602406 commit e5cb8c6
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 27 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@
This repository contains the backend code for the DisGitHook project.

It contains the API for the website and the code for handling webhooks.

## API routes

- /
- /login
- /logout
- /servers
- /server/:id (get & set settings)
- POST /hook/:id & Authorization header
- POST /hook/:id/:secret
9 changes: 8 additions & 1 deletion bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ const bot = new Discord.Client({

const { botToken } = require("./config.json")
bot.login(botToken)
module.exports = bot

const pool = require("./util/setupDB.js")

bot.on("ready", () => {
bot.user.setPresence({activities: [{name: "Custom Status", state: "Wooooo!", type: Discord.ActivityType.Custom}], status: "dnd"})
bot.user.setPresence({activities: [{name: "Custom Status", state: "Customizable GitHub hooks!", type: Discord.ActivityType.Custom}], status: "dnd"})
})

bot.on("guildDelete", guild => {
pool.query("DELETE FROM `hook` WHERE `guild` = ?", [guild.id])
})
63 changes: 56 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
const Discord = require("discord.js")
require("./bot.js")
const bot = require("./bot.js")

const { botId, botSecret, userAgent, domain, port, cookieSecret } = require("./config.json")

const oauth = require("./util/oauth.js")
const pool = require("./util/setupDB.js")

const crypto = require("node:crypto")
const express = require("express")
const cookieParser = require("cookie-parser")

const app = express()
app.disable("x-powered-by")
app.use(express.json())
app.use(cookieParser(cookieSecret))
app.listen(port)

Expand All @@ -22,10 +25,10 @@ app.get("/", (req, res) => {
app.get("/servers", async (req, res) => {
if (!req.signedCookies.token) return res.status(401).send("Missing token cookie")

const servers = await oauth.getUserServers()
const servers = await oauth.getUserServers(req.signedCookies.token, pool)
console.log(servers)

const filtered = servers.filter(server => Discord.PermissionsBitField(server.permissions).has("ManageGuild")).map(server => ({
const filtered = servers.filter(server => server.owner || Discord.PermissionsBitField(server.permissions).has("ManageGuild")).map(server => ({
id: server.id,
name: server.name,
icon: server.icon
Expand Down Expand Up @@ -89,11 +92,57 @@ app.get("/logout", (req, res) => {

const hookFunc = async (req, res) => {
const [rows] = await pool.query("SELECT * FROM `hook` WHERE `id` = ?", [req.params.id])
console.log(rows)

if (rows.length == 0) return res.sendStatus(404)

res.sendStatus(204)
const hook = rows[0]
console.log(req.body)

if (req.params.secret.startsWith("sha256=")) {
const sha256 = crypto.createHmac("sha256", hook.secret)
if (!crypto.timingSafeEqual(Buffer.from("sha256=" + sha256.update(JSON.stringify(req.body)).digest("hex")), Buffer.from(req.params.secret))) return res.status(401).send("Invalid secret as header")
} else if (req.params.secret != hook.secret) return res.status(401).send("Invalid secret in URL")

const githubEvent = req.headers["x-github-event"]
if (hook.filterEvent && !hook.filterEvent.includes(githubEvent)) return res.status(202).send("Event " + githubEvent + " is disabled in settings for this hook")

const data = req.body
const action = data.action
if (hook.filterAction && !hook.filterAction.includes(action)) return res.status(202).send("Action " + action + " is disabled in settings for this hook")

let message = hook.message
const recursiveFunc = (obj, path = "") => {
for (const property in obj) {
if (typeof obj[property] == "object") recursiveFunc(obj[property], path + property + ".")
else message = message.replace(new RegExp("{" + path + property + "}", "gi"), obj[property])
}
}
recursiveFunc(data)

let parsed = {}
try {
parsed = JSON.parse(message)

Check failure on line 123 in index.js

View workflow job for this annotation

GitHub Actions / Codestandards

'parsed' is assigned a value but never used. Allowed unused vars must match /_/u
} catch (e) {

Check failure on line 124 in index.js

View workflow job for this annotation

GitHub Actions / Codestandards

Remove unused catch binding `e`
return res.status(500).send("Invalid JSON in message")
}

if (hook.webhook) {
const webhookClient = new Discord.WebhookClient(hook.webhook, hook.secret)
await webhookClient.send(JSON.parse(message))
res.sendStatus(204)
} else {
const channel = bot.channels.cache.get(hook.channel)
if (channel) {
await channel.send(JSON.parse(message))
res.sendStatus(204)
} else res.status(500).send("Unable to send message of hook " + hook.id + " because the channel " + hook.channel + " does not exist")
}
}
app.get("/hook/:id/:secret", hookFunc)

app.post("/hook/:id/:secret", hookFunc)
app.post("/hook/:id", async (req, res) => {
console.log(req.headers)
if (!req.get("X-Hub-Signature-256")) return res.status(401).send("Missing X-Hub-Signature-256 header")

req.params.secret = req.get("X-Hub-Signature-256")
hookFunc(req, res)
})
27 changes: 14 additions & 13 deletions util/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,36 @@ module.exports.getUser = async token => {
}

const guildCache = {}
module.exports.getUserServers = async (userId, pool) => {
const token = await this.getAccessToken(userId, pool)
if (!token) return new Error("Couldnt get access token")
module.exports.getUserServers = async (token, pool) => {
const access = await this.getAccessToken(token, pool)
if (!access) return new Error("Couldnt get access token")

if (guildCache[token]) return guildCache[token]
if (guildCache[access]) return guildCache[access]
const res = await fetch("https://discord.com/api/v10/users/@me/guilds", {
headers: {
"User-Agent": userAgent,
Authorization: "Bearer " + token,
Authorization: "Bearer " + access,
Accept: "application/json"
}
})
if (!res.ok) return new Error("Couldnt get guild data, failed with " + res.status + " " + res.statusText)

const json = await res.json()
guildCache[token] = json
setTimeout(() => delete guildCache[token], 1000 * 60 * 10)
guildCache[access] = json
setTimeout(() => delete guildCache[access], 1000 * 60 * 10)
return json
}

module.exports.getAccessToken = async (userId, pool) => {
const token = pool.query("SELECT * FROM `user` WHERE `id` = ?", [userId])
module.exports.getAccessToken = async (token, pool) => {
const [rows] = await pool.query("SELECT * FROM `user` WHERE `token` = ?", [token])
const access = rows[0]

if (token && Date.now() > token.expires_at) {
if (access && Date.now() > access.expires_at) {
const body = new URLSearchParams({
client_id: botId,
client_secret: botSecret,
grant_type: "refresh_token",
refresh_token: token.refresh
refresh_token: access.refresh
})
const res = await fetch("https://discord.com/api/v10/oauth2/token", {
method: "POST",
Expand All @@ -61,7 +62,7 @@ module.exports.getAccessToken = async (userId, pool) => {
if (res.ok) {
const newToken = await res.json()
newToken.expires_at = Date.now() + newToken.expires_in * 1000
newToken.type = token.type
newToken.type = access.type
delete newToken.expires_in

pool.query(
Expand All @@ -76,7 +77,7 @@ module.exports.getAccessToken = async (userId, pool) => {
}
}

return token ? token.access : void 0
return access ? access.access : void 0
}

module.exports.generateToken = () => {
Expand Down
14 changes: 8 additions & 6 deletions util/setupDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ const mainDB = mysql.createPool(db)
const pool = mainDB.promise()
module.exports = pool

// hook schema: id secret guild webhook? channel? name? avatar? message
// hook schema: id secret guild webhook? channel? name? avatar? message filterEvent? filterAction?
pool.query(
"CREATE TABLE IF NOT EXISTS `hook` (" +
"`id` VARCHAR(8) NOT NULL PRIMARY KEY," +
"`secret` VARCHAR(64) NOT NULL," +
"`guild` VARCHAR(21) NOT NULL," +
"`webhook` VARCHAR(128) NOT NULL," +
"`channel` VARCHAR(21) NOT NULL," +
"`name` VARCHAR(32) NOT NULL," +
"`avatar` VARCHAR(512) NOT NULL," +
"`message` VARCHAR(10240) NOT NULL" +
"`channel` VARCHAR(21) NULL DEFAULT NULL," +
"`webhook` VARCHAR(128) NULL DEFAULT NULL," +
"`name` VARCHAR(32) NULL DEFAULT NULL," +
"`avatar` VARCHAR(512) NULL DEFAULT NULL," +
"`message` VARCHAR(10240) NOT NULL," +
"`filterEvent` VARCHAR(512) NULL DEFAULT NULL," +
"`filterAction` VARCHAR(512) NULL DEFAULT NULL" +
")"
)

Expand Down

0 comments on commit e5cb8c6

Please sign in to comment.