Compare commits
No commits in common. "fb721481e7b230dd80f5f2ef1d41fcb70cd06826" and "00a90e9d5ac1ea03ca207f47f32e2f2b4c093706" have entirely different histories.
fb721481e7
...
00a90e9d5a
13 changed files with 25 additions and 217 deletions
|
@ -1,4 +1,4 @@
|
|||
import { WebSocket, WebSocketServer } from "ws";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { guildMap } from "./common.js";
|
||||
import { decodeToken } from "./tokens.js";
|
||||
|
||||
|
@ -12,9 +12,7 @@ const messageTypes = {
|
|||
};
|
||||
|
||||
class GatewayServer {
|
||||
constructor() {}
|
||||
|
||||
start(server, extraWebsocketServerConfig={}) {
|
||||
constructor(server, extraWebsocketServerConfig={}) {
|
||||
this.wss = new WebSocketServer({
|
||||
server,
|
||||
...extraWebsocketServerConfig
|
||||
|
@ -101,7 +99,7 @@ class GatewayServer {
|
|||
try {
|
||||
message = JSON.parse(message.toString());
|
||||
} catch (e) {
|
||||
return ws.close(4000, "Payload JSON parse error.");
|
||||
return ws.close(4000, "Payload error.");
|
||||
}
|
||||
|
||||
if (!this._checkMessageSchema(message))
|
||||
|
@ -125,7 +123,7 @@ class GatewayServer {
|
|||
|
||||
this._clientDispatch(ws, {
|
||||
type: "SET_TOKEN",
|
||||
token: message.d.token
|
||||
token: message.token
|
||||
});
|
||||
this._clientDispatch(ws, {
|
||||
type: "AUTHENTICATE_AS",
|
||||
|
@ -207,30 +205,6 @@ class GatewayServer {
|
|||
ws.ping();
|
||||
});
|
||||
}
|
||||
|
||||
findClientByToken(token) {
|
||||
for (const [client, _] of this.wss.clients.entries()) {
|
||||
if (client.readyState === WebSocket.OPEN && client.state && client.state.token === token) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchHandoffFulfillment(targetToken, dispatchUUID, token) {
|
||||
const targetClient = this.findClientByToken(targetToken);
|
||||
if (!targetClient)
|
||||
return false;
|
||||
|
||||
targetClient.send(JSON.stringify({
|
||||
t: messageTypes.EVENT,
|
||||
d: {
|
||||
eventType: "$BRIDGE_HANDOFF_FULFILLMENT",
|
||||
dispatchUUID,
|
||||
token
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default GatewayServer;
|
|
@ -1,7 +1,6 @@
|
|||
export const mainHttpListenPort = 4050;
|
||||
export const watchedGuildIds = ["822089558886842418", "736292509134749807"];
|
||||
export const jwtSecret = process.env.JWT_SECRET;
|
||||
export const jwtHandoffSecret = process.env.JWT_HANDOFF_SECRET;
|
||||
export const discordToken = process.env.DISCORD_TOKEN;
|
||||
export const dangerousAdminMode = true;
|
||||
export const logContextMap = {
|
||||
|
@ -21,15 +20,3 @@ export const logContextMap = {
|
|||
error: true,
|
||||
}
|
||||
};
|
||||
|
||||
if (!jwtSecret) {
|
||||
console.error("Missing jwtSecret. Please make sure it's set through the environment variable JWT_SECRET.");
|
||||
}
|
||||
|
||||
if (!jwtHandoffSecret) {
|
||||
console.error("Missing jwtHandoffSecret. Please make sure it's set through the environment variable JWT_SECRET.");
|
||||
}
|
||||
|
||||
if (jwtSecret === jwtHandoffSecret) {
|
||||
console.error("jwtHandoffSecret and jwtSecret are both equal. This is a stability and security risk.");
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
hash = hash.substring(1, hash.length);
|
||||
if (hash !== "") {
|
||||
routeInfo = hash.split(",,");
|
||||
routeInfo = hash.split("||");
|
||||
}
|
||||
|
||||
function doChatLogin() {
|
||||
|
@ -185,26 +185,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function fuzzyConfirmHandoff({ detail: yn }) {
|
||||
if (yn === "Yes") {
|
||||
if (!apiClient.token) {
|
||||
view = { type: "MESSAGE_DISPLAY", header: "Authorization failed", content: "You need to be logged in to give applications account access." };
|
||||
return;
|
||||
}
|
||||
apiClient.postRequest("/tokens/handoff/fulfill", {
|
||||
handoffToken: view.handoffToken
|
||||
}).then(() => {
|
||||
view = { type: "MESSAGE_DISPLAY", header: "Application authorized", content: "You can now close this tab." };
|
||||
}).catch(() => {
|
||||
view = { type: "MESSAGE_DISPLAY", header: "Authorization failed", content: "We couldn't authorize this application." };
|
||||
});
|
||||
} else if (yn === "No") {
|
||||
view = { type: "MESSAGE_DISPLAY", header: "Got it!", content: "We won't give this application account access right now." };
|
||||
} else {
|
||||
view = { type: "MESSAGE_DISPLAY", header: "Authorization failed", content: "We can't give this application account access right now." };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (routeInfo.length >= 2) {
|
||||
switch (routeInfo[0]) {
|
||||
|
@ -212,10 +192,6 @@
|
|||
view = { type: "REDEEM_TOKEN_CONFIRM_PROMPT", token: routeInfo[1] };
|
||||
break;
|
||||
}
|
||||
case "token_handoff": {
|
||||
view = { type: "HANDOFF_TOKEN_CONFIRM_PROMPT", handoffToken: routeInfo[1] };
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no special route, continue normal execution
|
||||
|
@ -254,7 +230,5 @@
|
|||
<MessageDisplay header={ view.header } content={ view.content } ></MessageDisplay>
|
||||
{:else if view.type === "REDEEM_TOKEN_CONFIRM_PROMPT"}
|
||||
<FuzzyView on:selected={fuzzyConfirmToken} elements={[{name: "Yes", id: "Yes"}, {name: "No", id: "No"}]} title="Would you like to import this token? It might've been created for you by your bridge administrator, or a bridge application. Only import tokens you own and trust." />
|
||||
{:else if view.type === "HANDOFF_TOKEN_CONFIRM_PROMPT"}
|
||||
<FuzzyView on:selected={fuzzyConfirmHandoff} elements={[{name: "Yes", id: "Yes"}, {name: "No", id: "No"}]} title="An application would like to have access to your account. This means it will be able to read and send messages on your behalf. Once you give an application access, you will not be able to revoke it. Only allow account access to applications you trust. Would you like to give this application account access?" />
|
||||
{/if}
|
||||
</main>
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import { createServer } from "node:http";
|
||||
import { bot, logger } from "./common.js";
|
||||
import { mainHttpListenPort } from "./config.js";
|
||||
import http from "node:http";
|
||||
import express from "express";
|
||||
import apiRoute from "./routes/api.js";
|
||||
import { gatewayServer } from "./commonservers.js";
|
||||
import { mainHttpListenPort } from "./config.js";
|
||||
import { bot, logger } from "./common.js";
|
||||
import GatewayServer from "./GatewayServer.js";
|
||||
|
||||
// (probably) a bad idea
|
||||
const log = logger("log", "ServerMain");
|
||||
|
||||
// might introduce bugs and probably a bad idea
|
||||
Object.freeze(Object.prototype);
|
||||
Object.freeze(Object);
|
||||
|
||||
|
||||
const log = logger("log", "main");
|
||||
|
||||
export const app = express();
|
||||
export const httpServer = createServer(app);
|
||||
|
||||
gatewayServer.start(httpServer);
|
||||
const app = express();
|
||||
const httpServer = http.createServer(app);
|
||||
const gatewayServer = new GatewayServer(httpServer, {
|
||||
path: "/gateway"
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
app.use("/", express.static("frontend/public/"));
|
|
@ -1,14 +1,13 @@
|
|||
{
|
||||
"name": "discordbridge",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"express": "^4.17.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"node-fetch": "^3.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^8.4.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import express from "express";
|
||||
import { guildMap, logger } from "../common.js";
|
||||
import { dangerousAdminMode } from "../config.js";
|
||||
import { checkAuth, createHandoffToken, createToken, decodeHandoffToken } from "../tokens.js";
|
||||
import { v4 } from "uuid";
|
||||
import { gatewayServer } from "../commonservers.js";
|
||||
import { checkAuth, createToken } from "../tokens.js";
|
||||
|
||||
const error = logger("error", "API");
|
||||
|
||||
|
@ -166,41 +164,4 @@ router.get("/events/poll", checkAuth(async (req, res) => {
|
|||
}
|
||||
}));
|
||||
|
||||
router.post("/tokens/handoff/create", checkAuth(async (req, res) => {
|
||||
const { username, isSuperToken=false } = req.user;
|
||||
if (!isSuperToken)
|
||||
return res.status(403).send({ error: true, message: "ERROR_NO_SUPERTOKEN" });
|
||||
|
||||
try {
|
||||
const dispatchUUID = v4();
|
||||
const token = await createHandoffToken({ displayName: username, dispatchUUID, consumerToken: req.token });
|
||||
res.status(200).send({ error: false, message: "SUCCESS_HANDOFF_CREATED", token, dispatchUUID });
|
||||
} catch(e) {
|
||||
res.status(500).send({ error: true, message: "ERROR_TOKEN_CREATE_FAILURE" });
|
||||
}
|
||||
}));
|
||||
|
||||
router.post("/tokens/handoff/fulfill", checkAuth(async (req, res) => {
|
||||
const { isSuperToken=false } = req.user;
|
||||
if (isSuperToken)
|
||||
return res.status(403).send({ error: true, message: "ERROR_SUPERTOKEN_SECURITY" });
|
||||
|
||||
const handoffToken = req.body.handoffToken;
|
||||
if (!handoffToken) {
|
||||
return res.status(400).send({ error: true, message: "ERROR_BAD_HANDOFF_TOKEN" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { consumerToken, dispatchUUID } = await decodeHandoffToken(handoffToken);
|
||||
if (gatewayServer.dispatchHandoffFulfillment(consumerToken, dispatchUUID, req.token)) {
|
||||
res.status(200).send({ error: false, message: "SUCCESS_HANDOFF_FULFILLED" });
|
||||
} else {
|
||||
res.status(400).send({ error: true, message: "ERROR_NO_HANDOFF_CONSUMERS" });
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
res.status(400).send({ error: true, message: "ERROR_BAD_HANDOFF_TOKEN" });
|
||||
}
|
||||
}));
|
||||
|
||||
export default router;
|
|
@ -14,7 +14,6 @@ const TARGET_GUILD_ID = "_";
|
|||
const TARGET_CHANNEL_ID = "_";
|
||||
const ORIGIN = "http://localhost:4050";
|
||||
const GATEWAY_ORIGIN = "ws://localhost:4050/gateway";
|
||||
const FRONTEND_ORIGIN = "http://localhost:4050/";
|
||||
const messageSchema = { t: "number", d: "object" };
|
||||
const messageTypes = {
|
||||
HELLO: 0,
|
||||
|
@ -22,16 +21,12 @@ const messageTypes = {
|
|||
READY: 2,
|
||||
EVENT: 3
|
||||
};
|
||||
const EXPRIMENTAL_ACCOUNT_LINK = false;
|
||||
|
||||
const chatMessageRegex = /^\[(?:.*?)\]: \<(?<username>[a-zA-Z0-9]+)\> (?<message>.*)/;
|
||||
const joinNotificationRegex = /^\[(?:.*?)\]: (?<username>[a-zA-Z0-9]+) joined the game/;
|
||||
const leaveNotificationRegex = /^\[(?:.*?)\]: (?<username>[a-zA-Z0-9]+) left the game/;
|
||||
const rconConnection = new Rcon("localhost", "25575", process.env.RCON_PASSWORD);
|
||||
|
||||
const pendingHandoffs = new Map();
|
||||
const usernameToToken = new Map();
|
||||
|
||||
export default class GatewayClient {
|
||||
constructor(gatewayPath) {
|
||||
this.gatewayPath = gatewayPath;
|
||||
|
@ -153,11 +148,7 @@ async function sendBridgeMessageAs(guildId, channelId, content, username=undefin
|
|||
});
|
||||
}
|
||||
|
||||
async function sendMinecraftMessageAs(rcon, username, content, attachments, referencedMessage=null) {
|
||||
if (!attachments) {
|
||||
attachments = [];
|
||||
}
|
||||
|
||||
async function sendMinecraftMessageAs(rcon, username, content, attachments=[], referencedMessage=null) {
|
||||
const tellrawPayload = [
|
||||
{ text: "[" },
|
||||
{ text: `${username}`, color: "gray" },
|
||||
|
@ -197,26 +188,6 @@ async function sendMinecraftMessageAs(rcon, username, content, attachments, refe
|
|||
rcon.send(`tellraw @a ${JSON.stringify(tellrawPayload)}`);
|
||||
}
|
||||
|
||||
async function startHandoffForUser(username) {
|
||||
const handoffRes = await fetch(`${ORIGIN}/api/v1/tokens/handoff/create`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"authorization": TOKEN
|
||||
}
|
||||
});
|
||||
const { token: handoffToken, dispatchUUID } = (await handoffRes.json());
|
||||
|
||||
pendingHandoffs.set(dispatchUUID, username);
|
||||
// TODO: probably not a good idea
|
||||
setTimeout(() => {
|
||||
pendingHandoffs.delete(dispatchUUID);
|
||||
}, 40000);
|
||||
|
||||
return handoffToken;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
rconConnection.on("error", (e) => {
|
||||
console.error("rcon: got error", e);
|
||||
|
@ -227,18 +198,9 @@ async function main() {
|
|||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
const gateway = new GatewayClient(GATEWAY_ORIGIN);
|
||||
gateway.onEvent = (e) => {
|
||||
if (e.eventType === "$BRIDGE_HANDOFF_FULFILLMENT") {
|
||||
const username = pendingHandoffs.get(e.dispatchUUID);
|
||||
if (!username)
|
||||
return;
|
||||
|
||||
usernameToToken.set(username, e.token);
|
||||
|
||||
sendMinecraftMessageAs(rconConnection, "Minecraft Bridge System", `${username} has been successfully linked to a bridge token.`, null, null);
|
||||
} else if (e.eventType === "MESSAGE_CREATE" && e.message.channel_id === TARGET_CHANNEL_ID && e.message.guild_id === TARGET_GUILD_ID && !e.message.webhook_id) {
|
||||
if (e.eventType === "MESSAGE_CREATE" && e.message.channel_id === TARGET_CHANNEL_ID && e.message.guild_id === TARGET_GUILD_ID && !e.message.webhook_id) {
|
||||
sendMinecraftMessageAs(rconConnection, e.message.author.username, e.message.content, e.message.attachments, e.message.referenced_message);
|
||||
}
|
||||
};
|
||||
|
@ -264,27 +226,8 @@ async function main() {
|
|||
|
||||
const messageResult = chatMessageRegex.exec(stringData);
|
||||
if (messageResult) {
|
||||
if (EXPRIMENTAL_ACCOUNT_LINK && messageResult.groups.message === "!:minecraft-link") {
|
||||
try {
|
||||
const handoffToken = await startHandoffForUser(messageResult.groups.username);
|
||||
const handoffLink = `${FRONTEND_ORIGIN}#token_handoff,,${handoffToken}`;
|
||||
sendMinecraftMessageAs(
|
||||
rconConnection,
|
||||
"Minecraft Bridge System",
|
||||
`${messageResult.groups.username}: Click on the attached link to finish linking your account. You have 40 seconds.`,
|
||||
[
|
||||
{
|
||||
filename: `link account to ${messageResult.groups.username}`,
|
||||
proxy_url: handoffLink
|
||||
}
|
||||
]
|
||||
);
|
||||
} catch(e) {
|
||||
sendMinecraftMessageAs(rconConnection, "Minecraft Bridge System", `Failed to create link for ${messageResult.groups.username}.`, null, null);
|
||||
}
|
||||
} else {
|
||||
await sendBridgeMessageAs(TARGET_GUILD_ID, TARGET_CHANNEL_ID, messageResult.groups.message, messageResult.groups.username, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import GatewayServer from "./GatewayServer.js";
|
||||
|
||||
export const gatewayServer = new GatewayServer();
|
|
@ -1,5 +1,5 @@
|
|||
import jsonwebtoken from "jsonwebtoken";
|
||||
import { jwtHandoffSecret, jwtSecret } from "./config.js";
|
||||
import { jwtSecret } from "./config.js";
|
||||
|
||||
export function createToken({ username, avatarURL, discordID, guildAccess, isSuperToken=false }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -12,17 +12,6 @@ export function createToken({ username, avatarURL, discordID, guildAccess, isSup
|
|||
});
|
||||
}
|
||||
|
||||
export function createHandoffToken({ dispatchUUID, displayName, consumerToken }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
jsonwebtoken.sign({ dispatchUUID, displayName, consumerToken }, jwtHandoffSecret, (err, token) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
|
||||
resolve(token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function decodeToken(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
jsonwebtoken.verify(token, jwtSecret, (err, token) => {
|
||||
|
@ -34,17 +23,6 @@ export function decodeToken(token) {
|
|||
});
|
||||
}
|
||||
|
||||
export function decodeHandoffToken(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
jsonwebtoken.verify(token, jwtHandoffSecret, (err, token) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
|
||||
resolve(token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function checkAuth(callback) {
|
||||
return async (req, res) => {
|
||||
const token = req.get("authorization");
|
||||
|
@ -59,7 +37,6 @@ export function checkAuth(callback) {
|
|||
if (user) {
|
||||
req.user = user;
|
||||
req.authenticated = true;
|
||||
req.token = token;
|
||||
return await callback(req, res);
|
||||
} else {
|
||||
res.status(401).send({ error: true, message: "ERROR_UNAUTHORIZED" });
|
|
@ -470,11 +470,6 @@ utils-merge@1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
|
|
Loading…
Reference in a new issue