diff --git a/ebs/src/modules/config.ts b/ebs/src/modules/config.ts index e2a243f..c271b8a 100644 --- a/ebs/src/modules/config.ts +++ b/ebs/src/modules/config.ts @@ -1,10 +1,11 @@ import { Config } from "common/types"; -import { app } from "../index"; +import { app } from ".."; import { sendPubSubMessage } from "../util/pubsub"; -import { strToU8, compressSync, strFromU8 } from "fflate"; +import { compressSync, strFromU8, strToU8 } from "fflate"; import { getBannedUsers } from "../util/db"; import { asyncCatch } from "../util/middleware"; import { Webhooks } from "@octokit/webhooks"; +import { sendToLogger } from "../util/logger"; let activeConfig: Config | undefined; let configData: Config | undefined; @@ -27,6 +28,18 @@ async function fetchConfig(): Promise { console.error("Error when fetching config"); console.error(e); + sendToLogger({ + transactionToken: null, + userIdInsecure: null, + important: true, + fields: [ + { + header: "Error when fetching config", + content: e.toString(), + }, + ], + }).then(); + return { version: -1, message: "Error when fetching config", @@ -37,8 +50,7 @@ async function fetchConfig(): Promise { function processConfig(data: Config) { const config: Config = JSON.parse(JSON.stringify(data)); if (!ingameState) { - Object.values(config.redeems!) - .forEach((redeem) => (redeem.disabled = true)); + Object.values(config.redeems!).forEach((redeem) => (redeem.disabled = true)); } return config; } @@ -53,7 +65,7 @@ export async function getConfig(): Promise { export async function setActiveConfig(data: Config) { activeConfig = processConfig(data); - broadcastConfigRefresh(activeConfig); + await broadcastConfigRefresh(activeConfig); } export async function broadcastConfigRefresh(config: Config) { @@ -72,7 +84,7 @@ export function isIngame() { export function setIngame(newIngame: boolean) { if (ingameState == newIngame) return; ingameState = newIngame; - setActiveConfig(configData!); + setActiveConfig(configData!).then(); } async function refreshConfig() { @@ -80,26 +92,31 @@ async function refreshConfig() { activeConfig = processConfig(configData); } -app.get("/private/refresh", asyncCatch(async (_, res) => { - await refreshConfig(); - console.log("Refreshed config, new config version is ", activeConfig!.version); - await broadcastConfigRefresh(activeConfig!); - res.sendStatus(200); -})); +app.get( + "/private/refresh", + asyncCatch(async (_, res) => { + await refreshConfig(); + console.log("Refreshed config, new config version is ", activeConfig!.version); + await broadcastConfigRefresh(activeConfig!); + res.sendStatus(200); + }) +); const webhooks = new Webhooks({ secret: process.env.PRIVATE_API_KEY!, }); -app.post("/webhook/refresh", asyncCatch(async (req, res) => { - // github webhook - const signature = req.headers["x-hub-signature-256"] as string; - const body = JSON.stringify(req.body); +app.post( + "/webhook/refresh", + asyncCatch(async (req, res) => { + // github webhook + const signature = req.headers["x-hub-signature-256"] as string; + const body = JSON.stringify(req.body); - if(!(await webhooks.verify(body, signature))) { - res.sendStatus(403); - return; - } + if (!(await webhooks.verify(body, signature))) { + res.sendStatus(403); + return; + } // only refresh if the config.json file was changed if(req.body.commits.some((commit: any) => commit.modified.includes("config.json"))) { @@ -113,10 +130,13 @@ app.post("/webhook/refresh", asyncCatch(async (req, res) => { } })); -app.get("/public/config", asyncCatch(async (req, res) => { - const config = await getConfig(); - res.send(JSON.stringify(config)); -})); +app.get( + "/public/config", + asyncCatch(async (req, res) => { + const config = await getConfig(); + res.send(JSON.stringify(config)); + }) +); (async () => { const config = await getConfig(); diff --git a/ebs/src/modules/game/index.ts b/ebs/src/modules/game/index.ts index cbd115f..8ad2e9d 100644 --- a/ebs/src/modules/game/index.ts +++ b/ebs/src/modules/game/index.ts @@ -1,10 +1,10 @@ -import { app } from "../../index"; +import { app } from "../.."; import { asyncCatch } from "../../util/middleware"; import { GameConnection } from "./connection"; import { MessageType } from "./messages"; import { ResultMessage } from "./messages.game"; import { CommandInvocationSource, RedeemMessage } from "./messages.server"; -import { StressTestRequest, isStressTesting, startStressTest } from "./stresstest"; +import { isStressTesting, startStressTest, StressTestRequest } from "./stresstest"; export let connection: GameConnection = new GameConnection(); @@ -12,25 +12,28 @@ app.ws("/private/socket", (ws, req) => { connection.setSocket(ws); }); -app.post("/private/redeem", asyncCatch(async (req, res) => { - //console.log(req.body); - const msg = { - ...connection.makeMessage(MessageType.Redeem), - source: CommandInvocationSource.Dev, - ...req.body, - } as RedeemMessage; - if (!connection.isConnected()) { - res.status(500).send("Not connected"); - return; - } +app.post( + "/private/redeem", + asyncCatch(async (req, res) => { + //console.log(req.body); + const msg = { + ...connection.makeMessage(MessageType.Redeem), + source: CommandInvocationSource.Dev, + ...req.body, + } as RedeemMessage; + if (!connection.isConnected()) { + res.status(500).send("Not connected"); + return; + } - try { - await connection.sendMessage(msg); - res.status(201).send(JSON.stringify(msg)); - } catch (e) { - res.status(500).send(e); - } -})); + try { + await connection.sendMessage(msg); + res.status(201).send(JSON.stringify(msg)); + } catch (e) { + res.status(500).send(e); + } + }) +); app.post("/private/setresult", (req, res) => { //console.log(req.body); @@ -62,7 +65,7 @@ app.post("/private/stress", (req, res) => { res.status(500).send("Not connected"); return; } - + const reqObj = req.body as StressTestRequest; if (reqObj.type === undefined || reqObj.duration === undefined || reqObj.interval === undefined) { res.status(400).send("Must have type, duration, and interval"); @@ -71,14 +74,14 @@ app.post("/private/stress", (req, res) => { console.log(reqObj); startStressTest(reqObj.type, reqObj.duration, reqObj.interval); res.sendStatus(200); -}) +}); app.get("/private/unsent", (req, res) => { const unsent = connection.getUnsent(); res.send(JSON.stringify(unsent)); -}) +}); app.get("/private/outstanding", (req, res) => { const outstanding = connection.getOutstanding(); res.send(JSON.stringify(outstanding)); -}) +}); diff --git a/ebs/src/modules/transactions.ts b/ebs/src/modules/transactions.ts index 6ed03cf..0654a14 100644 --- a/ebs/src/modules/transactions.ts +++ b/ebs/src/modules/transactions.ts @@ -1,276 +1,282 @@ import { Cart, Config, LogMessage, Transaction } from "common/types"; -import { app } from "../index"; +import { app } from ".."; import { parseJWT, verifyJWT } from "../util/jwt"; import { BitsTransactionPayload } from "../types"; import { getConfig } from "./config"; -import { getPrepurchase, isReceiptUsed, isUserBanned, registerPrepurchase, deletePrepurchase } from "../util/db"; -import { logToDiscord } from "../util/logger"; +import { deletePrepurchase, getPrepurchase, isReceiptUsed, isUserBanned, registerPrepurchase } from "../util/db"; +import { sendToLogger } from "../util/logger"; import { connection } from "./game"; import { TwitchUser } from "./game/messages"; import { getHelixUser } from "../util/twitch"; import { asyncCatch } from "../util/middleware"; -app.post("/public/prepurchase", asyncCatch(async (req, res) => { - const cart = req.body as Cart; - const idCart = { ...cart, userId: req.twitchAuthorization!.user_id! }; +app.post( + "/public/prepurchase", + asyncCatch(async (req, res) => { + const cart = req.body as Cart; + const idCart = { ...cart, userId: req.twitchAuthorization!.user_id! }; - if (await isUserBanned(req.twitchAuthorization!.user_id!)) { - res.status(403).send("You are banned from using this extension."); - return; - } + if (await isUserBanned(req.twitchAuthorization!.user_id!)) { + res.status(403).send("You are banned from using this extension."); + return; + } - if (await isUserBanned(req.twitchAuthorization!.opaque_user_id!)) { - res.status(403).send("You are banned from using this extension."); - return; - } + if (await isUserBanned(req.twitchAuthorization!.opaque_user_id!)) { + res.status(403).send("You are banned from using this extension."); + return; + } - if (!connection.isConnected()) { - res.status(502).send("Game connection is not available"); - return; - } + if (!connection.isConnected()) { + res.status(502).send("Game connection is not available"); + return; + } - const logContext: LogMessage = { - transactionToken: null, - userIdInsecure: idCart.userId, - important: false, - fields: [ - { - header: "", - content: "", - }, - ], - }; - const logMessage = logContext.fields[0]; - - const config = await getConfig(); - if (cart.version != config.version) { - logMessage.header = "Invalid config version"; - logMessage.content = `Received: ${cart.version}\nExpected: ${config.version}`; - logToDiscord(logContext).then(); - res.status(409).send(`Invalid config version (${cart.version}/${config.version})`); - return; - } + const logContext: LogMessage = { + transactionToken: null, + userIdInsecure: idCart.userId, + important: false, + fields: [ + { + header: "", + content: "", + }, + ], + }; + const logMessage = logContext.fields[0]; + + const config = await getConfig(); + if (cart.version != config.version) { + logMessage.header = "Invalid config version"; + logMessage.content = `Received: ${cart.version}\nExpected: ${config.version}`; + sendToLogger(logContext).then(); + res.status(409).send(`Invalid config version (${cart.version}/${config.version})`); + return; + } - const redeem = config.redeems?.[cart.id]; - if (!redeem - || redeem.sku != cart.sku - || redeem.disabled || redeem.hidden - ) { - logMessage.header = "Invalid redeem"; - logMessage.content = `Received: ${JSON.stringify(cart)}\nRedeem in config: ${JSON.stringify(redeem)}`; - logToDiscord(logContext).then(); - res.status(409).send(`Invalid redeem`); - return; - } + const redeem = config.redeems?.[cart.id]; + if (!redeem || redeem.sku != cart.sku || redeem.disabled || redeem.hidden) { + logMessage.header = "Invalid redeem"; + logMessage.content = `Received: ${JSON.stringify(cart)}\nRedeem in config: ${JSON.stringify(redeem)}`; + sendToLogger(logContext).then(); + res.status(409).send(`Invalid redeem`); + return; + } - const valError = validateArgs(config, cart, logContext); - if (valError) { - logMessage.header = "Arg validation failed"; - logMessage.content = { - error: valError, - redeem: cart.id, - expected: redeem.args, - provided: cart.args, - }; - logToDiscord(logContext).then(); - res.status(409).send("Invalid arguments"); - return; - } + const valError = validateArgs(config, cart, logContext); + if (valError) { + logMessage.header = "Arg validation failed"; + logMessage.content = { + error: valError, + redeem: cart.id, + expected: redeem.args, + provided: cart.args, + }; + sendToLogger(logContext).then(); + res.status(409).send("Invalid arguments"); + return; + } - // TODO: text input moderation - - let token: string; - try { - token = await registerPrepurchase(idCart); - } catch (e: any) { - logContext.important = true; - logMessage.header = "Failed to register prepurchase"; - logMessage.content = { cart: idCart, error: e }; - logToDiscord(logContext).then(); - res.status(500).send("Failed to register prepurchase"); - return; - } + // TODO: text input moderation + + let token: string; + try { + token = await registerPrepurchase(idCart); + } catch (e: any) { + logContext.important = true; + logMessage.header = "Failed to register prepurchase"; + logMessage.content = { cart: idCart, error: e }; + sendToLogger(logContext).then(); + res.status(500).send("Failed to register prepurchase"); + return; + } - logMessage.header = "Created prepurchase"; - logMessage.content = { cart: idCart }; - logToDiscord(logContext).then(); - - res.status(200).send(token); -})); - -app.post("/public/transaction", asyncCatch(async (req, res) => { - const transaction = req.body as Transaction; - - const logContext: LogMessage = { - transactionToken: transaction.token, - userIdInsecure: req.twitchAuthorization!.user_id!, - important: true, - fields: [ - { - header: "", - content: "", - }, - ], - }; - const logMessage = logContext.fields[0]; - - if (!transaction.receipt) { - logMessage.header = "Missing receipt"; - logMessage.content = transaction; - logToDiscord(logContext).then(); - res.status(400).send("Missing receipt"); - return; - } + logMessage.header = "Created prepurchase"; + logMessage.content = { cart: idCart }; + sendToLogger(logContext).then(); - if (!verifyJWT(transaction.receipt)) { - logMessage.header = "Invalid receipt"; - logMessage.content = transaction; - logToDiscord(logContext).then(); - res.status(403).send("Invalid receipt."); - return; - } + res.status(200).send(token); + }) +); - const payload = parseJWT(transaction.receipt) as BitsTransactionPayload; +app.post( + "/public/transaction", + asyncCatch(async (req, res) => { + const transaction = req.body as Transaction; - if (await isReceiptUsed(payload.data.transactionId)) { - logMessage.header = "Transaction already processed"; - logMessage.content = transaction; - logToDiscord(logContext).then(); - res.status(409).send("Transaction already processed"); - return; - } + const logContext: LogMessage = { + transactionToken: transaction.token, + userIdInsecure: req.twitchAuthorization!.user_id!, + important: true, + fields: [ + { + header: "", + content: "", + }, + ], + }; + const logMessage = logContext.fields[0]; + + if (!transaction.receipt) { + logMessage.header = "Missing receipt"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(400).send("Missing receipt"); + return; + } - const cart = await getPrepurchase(transaction.token); + if (!verifyJWT(transaction.receipt)) { + logMessage.header = "Invalid receipt"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(403).send("Invalid receipt."); + return; + } - if (!cart) { - logMessage.header = "Invalid transaction token"; - logMessage.content = transaction; - logToDiscord(logContext).then(); - res.status(404).send("Invalid transaction token"); - return; - } + const payload = parseJWT(transaction.receipt) as BitsTransactionPayload; - // TODO: mark transaction fulfilled + if (await isReceiptUsed(payload.data.transactionId)) { + logMessage.header = "Transaction already processed"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(409).send("Transaction already processed"); + return; + } + + const cart = await getPrepurchase(transaction.token); - if (cart.userId != req.twitchAuthorization!.user_id!) { - logContext.important = false; - logMessage.header = "Mismatched user ID"; - logMessage.content = { - auth: req.twitchAuthorization, - cart, - transaction, + if (!cart) { + logMessage.header = "Invalid transaction token"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(404).send("Invalid transaction token"); + return; } - logToDiscord(logContext).then(); - } - const currentConfig = await getConfig(); - if (cart.version != currentConfig.version) { - logContext.important = false; - logMessage.header = "Mismatched config version"; - logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, - }; - logToDiscord(logContext).then(); - } + // TODO: mark transaction fulfilled - console.log(transaction); - console.log(cart); - - const redeem = currentConfig.redeems?.[cart.id]; - if (!redeem) { - logContext.important = false; - logMessage.header = "Redeem not found"; - logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, - }; - logToDiscord(logContext).then(); - res.status(500).send("Redeem could not be found"); - return; - } + if (cart.userId != req.twitchAuthorization!.user_id!) { + logContext.important = false; + logMessage.header = "Mismatched user ID"; + logMessage.content = { + auth: req.twitchAuthorization, + cart, + transaction, + }; + sendToLogger(logContext).then(); + } - let userInfo: TwitchUser | null; - try { - userInfo = await getTwitchUser(cart.userId); - } catch { - userInfo = null; - } - if (!userInfo) { - logContext.important = false; - logMessage.header = "Could not get Twitch user info"; - logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, - error: userInfo, - }; - logToDiscord(logContext).then(); - // very much not ideal but they've already paid... so... - userInfo = { - id: cart.userId, - login: cart.userId, - displayName: cart.userId, - }; - } - try { - const resMsg = await connection.redeem(redeem, cart, userInfo, transaction.token); - if (resMsg?.success) { - console.log(`[${resMsg.guid}] Redeem succeeded: ${JSON.stringify(resMsg)}`); - let msg = "Your transaction was successful! Your redeem will appear on stream soon."; - if (resMsg.message) { - msg += "\n\n" + resMsg.message; - } - res.status(200).send(msg); - } else { + const currentConfig = await getConfig(); + if (cart.version != currentConfig.version) { logContext.important = false; - logMessage.header = "Redeem did not succeed"; - logMessage.content = resMsg; - logToDiscord(logContext); - res.status(500).send(resMsg?.message ?? "Redeem failed"); + logMessage.header = "Mismatched config version"; + logMessage.content = { + config: currentConfig.version, + cart: cart, + transaction: transaction, + }; + sendToLogger(logContext).then(); } - } catch (error) { - logContext.important = true; - logMessage.header = "Failed to send redeem"; - logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, - error: error, - }; - logToDiscord(logContext).then(); - res.status(500).send(`Failed to process redeem - ${error}`); - } -})); -app.post("/public/transaction/cancel", asyncCatch(async (req, res) => { - const token = req.body.token as string; + console.log(transaction); + console.log(cart); - // remove transaction from db - try { - await deletePrepurchase(token); + const redeem = currentConfig.redeems?.[cart.id]; + if (!redeem) { + logContext.important = false; + logMessage.header = "Redeem not found"; + logMessage.content = { + config: currentConfig.version, + cart: cart, + transaction: transaction, + }; + sendToLogger(logContext).then(); + res.status(500).send("Redeem could not be found"); + return; + } - res.sendStatus(200); - } catch (error) { - logToDiscord({ - transactionToken: token, - userIdInsecure: req.twitchAuthorization!.user_id!, - important: false, - fields: [ - { - header: "Error deleting transaction", - content: { - error: error, + let userInfo: TwitchUser | null; + try { + userInfo = await getTwitchUser(cart.userId); + } catch { + userInfo = null; + } + if (!userInfo) { + logContext.important = false; + logMessage.header = "Could not get Twitch user info"; + logMessage.content = { + config: currentConfig.version, + cart: cart, + transaction: transaction, + error: userInfo, + }; + sendToLogger(logContext).then(); + // very much not ideal but they've already paid... so... + userInfo = { + id: cart.userId, + login: cart.userId, + displayName: cart.userId, + }; + } + try { + const resMsg = await connection.redeem(redeem, cart, userInfo, transaction.token); + if (resMsg?.success) { + console.log(`[${resMsg.guid}] Redeem succeeded: ${JSON.stringify(resMsg)}`); + let msg = "Your transaction was successful! Your redeem will appear on stream soon."; + if (resMsg.message) { + msg += "\n\n" + resMsg.message; + } + res.status(200).send(msg); + } else { + logContext.important = false; + logMessage.header = "Redeem did not succeed"; + logMessage.content = resMsg; + sendToLogger(logContext); + res.status(500).send(resMsg?.message ?? "Redeem failed"); + } + } catch (error) { + logContext.important = true; + logMessage.header = "Failed to send redeem"; + logMessage.content = { + config: currentConfig.version, + cart: cart, + transaction: transaction, + error: error, + }; + sendToLogger(logContext).then(); + res.status(500).send(`Failed to process redeem - ${error}`); + } + }) +); + +app.post( + "/public/transaction/cancel", + asyncCatch(async (req, res) => { + const token = req.body.token as string; + + // remove transaction from db + try { + await deletePrepurchase(token); + + res.sendStatus(200); + } catch (error) { + sendToLogger({ + transactionToken: token, + userIdInsecure: req.twitchAuthorization!.user_id!, + important: false, + fields: [ + { + header: "Error deleting transaction", + content: { + error: error, + }, }, - }, - ], - }); + ], + }).then(); - res.sendStatus(404); - } -})); + res.sendStatus(404); + } + }) +); async function getTwitchUser(id: string): Promise { const user = await getHelixUser(id); @@ -281,7 +287,7 @@ async function getTwitchUser(id: string): Promise { id: user.id, displayName: user.displayName, login: user.name, - } + }; } function validateArgs(config: Config, cart: Cart, logContext: LogMessage): string | undefined { @@ -297,7 +303,7 @@ function validateArgs(config: Config, cart: Cart, logContext: LogMessage): strin // HTML form conventions - false is not transmitted, true is "on" (to save 2 bytes i'm guessing) continue; } - + return `Missing required argument ${arg.name}`; } let parsed: number; @@ -310,7 +316,7 @@ function validateArgs(config: Config, cart: Cart, logContext: LogMessage): strin } const minLength = arg.minLength ?? 0; const maxLength = arg.maxLength ?? 255; - if ((value.length < minLength || value.length > maxLength)) { + if (value.length < minLength || value.length > maxLength) { return `Text length out of range for ${arg.name}`; } break; diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index aced376..d2a2fc0 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -53,8 +53,8 @@ export async function setupDb() { await db.query(` CREATE TABLE IF NOT EXISTS logs ( id INT PRIMARY KEY AUTO_INCREMENT, - userId VARCHAR(255) NOT NULL, - transactionToken VARCHAR(255) NOT NULL, + userId VARCHAR(255), + transactionToken VARCHAR(255), data TEXT NOT NULL, fromBackend BOOLEAN NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP diff --git a/ebs/src/util/logger.ts b/ebs/src/util/logger.ts index 01bb052..075fcd7 100644 --- a/ebs/src/util/logger.ts +++ b/ebs/src/util/logger.ts @@ -2,7 +2,7 @@ import { LogMessage } from "common/types"; const logEndpoint = `http://${process.env.LOGGER_HOST!}:3000/log`; -export async function logToDiscord(data: LogMessage) { +export async function sendToLogger(data: LogMessage) { try { const result = await fetch(logEndpoint, { method: "POST", diff --git a/ebs/src/util/middleware.ts b/ebs/src/util/middleware.ts index 17d24c0..ce75b68 100644 --- a/ebs/src/util/middleware.ts +++ b/ebs/src/util/middleware.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express"; import { parseJWT, verifyJWT } from "./jwt"; import { AuthorizationPayload } from "../types"; -import { logToDiscord } from "./logger"; +import { sendToLogger } from "./logger"; export function publicApiAuth(req: Request, res: Response, next: NextFunction) { const auth = req.header("Authorization"); @@ -20,7 +20,7 @@ export function publicApiAuth(req: Request, res: Response, next: NextFunction) { req.twitchAuthorization = parseJWT(token) as AuthorizationPayload; if (!req.twitchAuthorization.user_id) { - logToDiscord({ + sendToLogger({ transactionToken: null, userIdInsecure: null, important: false, @@ -61,6 +61,20 @@ export function asyncCatch(fn: (req: Request, res: Response, next: NextFunction) try { await fn(req, res, next); } catch (err) { + console.log(err); + + sendToLogger({ + transactionToken: null, + userIdInsecure: null, + important: true, + fields: [ + { + header: "Error in asyncCatch", + content: err, + }, + ], + }).then(); + next(err); } }; diff --git a/logger/src/modules/endpoints.ts b/logger/src/modules/endpoints.ts index 9598b7c..7d81a3f 100644 --- a/logger/src/modules/endpoints.ts +++ b/logger/src/modules/endpoints.ts @@ -1,4 +1,4 @@ -import { app } from "../index"; +import { app } from ".."; import { logToDiscord } from "../util/discord"; import { LogMessage } from "common/types"; import { canLog, getUserIdFromTransactionToken, isUserBanned, logToDatabase } from "../util/db"; diff --git a/logger/src/util/db.ts b/logger/src/util/db.ts index d1ca2bc..49bdb7e 100644 --- a/logger/src/util/db.ts +++ b/logger/src/util/db.ts @@ -1,4 +1,4 @@ -import { db } from "../index"; +import { db } from ".."; import { RowDataPacket } from "mysql2"; import { LogMessage } from "common/types"; import { stringify } from "./stringify";