mirror of
https://github.com/ApfelTeeSaft/Reload-Backend.git
synced 2026-09-03 12:03:34 +00:00
555 lines
23 KiB
JavaScript
555 lines
23 KiB
JavaScript
const WebSocket = require("ws").Server;
|
|
const XMLBuilder = require("xmlbuilder");
|
|
const XMLParser = require("xml-parser");
|
|
const express = require("express");
|
|
const fs = require("fs");
|
|
const https = require("https");
|
|
const config = JSON.parse(fs.readFileSync("./Config/config.json").toString());
|
|
|
|
const app = express();
|
|
const log = require("../structs/log.js");
|
|
const functions = require("../structs/functions.js");
|
|
const User = require("../model/user.js");
|
|
const Friends = require("../model/friends.js");
|
|
const matchmaker = require("../matchmaker/matchmaker.js")
|
|
|
|
const port = config.bEnableHTTPS ? 443 : 80;
|
|
let wss;
|
|
|
|
let httpsOptions;
|
|
if (config.bEnableHTTPS) {
|
|
httpsOptions = {
|
|
cert: fs.readFileSync(config.ssl.cert),
|
|
ca: fs.existsSync(config.ssl.ca) ? fs.readFileSync(config.ssl.ca) : undefined,
|
|
key: fs.readFileSync(config.ssl.key)
|
|
};
|
|
}
|
|
|
|
if (config.bEnableHTTPS) {
|
|
const httpsServer = https.createServer(httpsOptions, app);
|
|
wss = new WebSocket({ server: httpsServer });
|
|
httpsServer.listen(port, () => {
|
|
log.xmpp(`XMPP and Matchmaker started listening on port ${port} (SSL Enabled)`);
|
|
});
|
|
} else {
|
|
wss = new WebSocket({ server: app.listen(port) });
|
|
log.xmpp(`XMPP and Matchmaker started listening on port ${port} (SSL Disabled)`);
|
|
}
|
|
|
|
global.xmppDomain = "prod.ol.epicgames.com";
|
|
global.Clients = [];
|
|
global.MUCs = {};
|
|
|
|
app.get("/", (req, res) => {
|
|
res.type("application/json");
|
|
res.header("Access-Control-Allow-Origin", "*");
|
|
|
|
let data = JSON.stringify({
|
|
"Clients": {
|
|
"amount": global.Clients.length,
|
|
"clients": global.Clients.map(i => i.displayName)
|
|
}
|
|
}, null, 2);
|
|
|
|
res.send(data);
|
|
});
|
|
|
|
app.get("/clients", (req, res) => {
|
|
res.type("application/json");
|
|
|
|
let data = JSON.stringify({
|
|
"amount": global.Clients.length,
|
|
"clients": global.Clients.map(i => i.displayName)
|
|
}, null, 2);
|
|
|
|
res.send(data);
|
|
});
|
|
|
|
wss.on('listening', () => {
|
|
|
|
});
|
|
|
|
wss.on('connection', async (ws) => {
|
|
ws.on('error', () => {});
|
|
|
|
// Start matchmaker if it's not connecting for xmpp.
|
|
if (ws.protocol.toLowerCase() != "xmpp") return matchmaker(ws);
|
|
|
|
let joinedMUCs = [];
|
|
let accountId = "";
|
|
let displayName = "";
|
|
let token = "";
|
|
let jid = "";
|
|
let resource = "";
|
|
let ID = "";
|
|
let Authenticated = false;
|
|
let clientExists = false;
|
|
let connectionClosed = false;
|
|
|
|
ws.on('message', async (message) => {
|
|
if (Buffer.isBuffer(message)) message = message.toString();
|
|
|
|
const msg = XMLParser(message);
|
|
if (!msg || !msg.root || !msg.root.name) return Error(ws);
|
|
|
|
switch (msg.root.name) {
|
|
case "open":
|
|
if (!ID) ID = functions.MakeID();
|
|
|
|
ws.send(XMLBuilder.create("open")
|
|
.attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-framing")
|
|
.attribute("from", global.xmppDomain)
|
|
.attribute("id", ID)
|
|
.attribute("version", "1.0")
|
|
.attribute("xml:lang", "en").toString());
|
|
|
|
if (Authenticated) {
|
|
ws.send(XMLBuilder.create("stream:features").attribute("xmlns:stream", "http://etherx.jabber.org/streams")
|
|
.element("ver").attribute("xmlns", "urn:xmpp:features:rosterver").up()
|
|
.element("starttls").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls").up()
|
|
.element("bind").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind").up()
|
|
.element("compression").attribute("xmlns", "http://jabber.org/features/compress")
|
|
.element("method", "zlib").up().up()
|
|
.element("session").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-session").up().toString());
|
|
} else {
|
|
ws.send(XMLBuilder.create("stream:features").attribute("xmlns:stream", "http://etherx.jabber.org/streams")
|
|
.element("mechanisms").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl")
|
|
.element("mechanism", "PLAIN").up().up()
|
|
.element("ver").attribute("xmlns", "urn:xmpp:features:rosterver").up()
|
|
.element("starttls").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls").up()
|
|
.element("compression").attribute("xmlns", "http://jabber.org/features/compress")
|
|
.element("method", "zlib").up().up()
|
|
.element("auth").attribute("xmlns", "http://jabber.org/features/iq-auth").up().toString());
|
|
}
|
|
break;
|
|
|
|
case "auth":
|
|
if (!ID) return;
|
|
if (accountId) return;
|
|
if (!msg.root.content) return Error(ws);
|
|
if (!functions.DecodeBase64(msg.root.content).includes("\u0000")) return Error(ws);
|
|
|
|
let decodedBase64 = functions.DecodeBase64(msg.root.content).split("\u0000");
|
|
|
|
let object = global.accessTokens.find(i => i.token == decodedBase64[2]);
|
|
if (!object) return Error(ws);
|
|
|
|
if (global.Clients.find(i => i.accountId == object.accountId)) return Error(ws);
|
|
|
|
let user = await User.findOne({ accountId: object.accountId, banned: false }).lean();
|
|
if (!user) return Error(ws);
|
|
|
|
accountId = user.accountId;
|
|
displayName = user.username;
|
|
token = object.token;
|
|
|
|
if (decodedBase64 && accountId && displayName && token && decodedBase64.length == 3) {
|
|
Authenticated = true;
|
|
log.xmpp(`An xmpp client with the displayName ${displayName} has logged in.`);
|
|
|
|
ws.send(XMLBuilder.create("success").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl").toString());
|
|
} else return Error(ws);
|
|
break;
|
|
|
|
case "iq":
|
|
if (!ID) return;
|
|
|
|
switch (msg.root.attributes.id) {
|
|
case "_xmpp_bind1":
|
|
if (resource || !accountId) return;
|
|
if (!msg.root.children.find(i => i.name == "bind")) return;
|
|
|
|
if (global.Clients.find(i => i.accountId == accountId)) return Error(ws);
|
|
|
|
let findResource = msg.root.children.find(i => i.name == "bind").children.find(i => i.name == "resource");
|
|
|
|
if (!findResource) return;
|
|
if (!findResource.content) return;
|
|
|
|
resource = findResource.content;
|
|
jid = `${accountId}@${global.xmppDomain}/${resource}`;
|
|
|
|
ws.send(XMLBuilder.create("iq")
|
|
.attribute("to", jid)
|
|
.attribute("id", "_xmpp_bind1")
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "result")
|
|
.element("bind")
|
|
.attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind")
|
|
.element("jid", jid).up().up().toString());
|
|
break;
|
|
|
|
case "_xmpp_session1":
|
|
if (!clientExists) return Error(ws);
|
|
|
|
ws.send(XMLBuilder.create("iq")
|
|
.attribute("to", jid)
|
|
.attribute("from", global.xmppDomain)
|
|
.attribute("id", "_xmpp_session1")
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "result").toString());
|
|
|
|
await getPresenceFromFriends(ws, accountId, jid);
|
|
break;
|
|
|
|
default:
|
|
if (!clientExists) return Error(ws);
|
|
|
|
ws.send(XMLBuilder.create("iq")
|
|
.attribute("to", jid)
|
|
.attribute("from", global.xmppDomain)
|
|
.attribute("id", msg.root.attributes.id)
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "result").toString());
|
|
}
|
|
break;
|
|
|
|
case "message":
|
|
if (!clientExists) return Error(ws);
|
|
|
|
let findBody = msg.root.children.find(i => i.name == "body");
|
|
|
|
if (!findBody || !findBody.content) return;
|
|
|
|
let body = findBody.content;
|
|
|
|
switch (msg.root.attributes.type) {
|
|
case "chat":
|
|
if (!msg.root.attributes.to) return;
|
|
if (body.length >= 300) return;
|
|
|
|
let receiver = global.Clients.find(i => i.jid.split("/")[0] == msg.root.attributes.to);
|
|
|
|
if (!receiver) return;
|
|
if (receiver.accountId == accountId) return;
|
|
|
|
receiver.client.send(XMLBuilder.create("message")
|
|
.attribute("to", receiver.jid)
|
|
.attribute("from", jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "chat")
|
|
.element("body", body).up().toString());
|
|
return;
|
|
|
|
case "groupchat":
|
|
if (!msg.root.attributes.to) return;
|
|
if (body.length >= 300) return;
|
|
|
|
let roomName = msg.root.attributes.to.split("@")[0];
|
|
|
|
let MUC = global.MUCs[roomName];
|
|
if (!MUC) return;
|
|
|
|
if (!MUC.members.find(i => i.accountId == accountId)) return;
|
|
|
|
MUC.members.forEach(member => {
|
|
let ClientData = global.Clients.find(i => i.accountId == member.accountId);
|
|
if (!ClientData) return;
|
|
|
|
ClientData.client.send(XMLBuilder.create("message")
|
|
.attribute("to", ClientData.jid)
|
|
.attribute("from", getMUCmember(roomName, displayName, accountId, resource))
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "groupchat")
|
|
.element("body", body).up().toString());
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isJSON(body)) {
|
|
let bodyJSON = JSON.parse(body);
|
|
|
|
if (Array.isArray(bodyJSON)) return;
|
|
if (typeof bodyJSON.type != "string") return;
|
|
if (!msg.root.attributes.to) return;
|
|
if (!msg.root.attributes.id) return;
|
|
|
|
sendXmppMessageToClient(jid, msg, body);
|
|
}
|
|
break;
|
|
|
|
case "presence":
|
|
if (!clientExists) return Error(ws);
|
|
|
|
switch (msg.root.attributes.type) {
|
|
case "unavailable":
|
|
if (!msg.root.attributes.to) return;
|
|
|
|
if (msg.root.attributes.to.endsWith(`@muc.${global.xmppDomain}`) || msg.root.attributes.to.split("/")[0].endsWith(`@muc.${global.xmppDomain}`)) {
|
|
if (!msg.root.attributes.to.toLowerCase().startsWith("party-")) return;
|
|
|
|
let roomName = msg.root.attributes.to.split("@")[0];
|
|
|
|
if (!global.MUCs[roomName]) return;
|
|
|
|
let memberIndex = global.MUCs[roomName].members.findIndex(i => i.accountId == accountId);
|
|
if (memberIndex != -1) {
|
|
global.MUCs[roomName].members.splice(memberIndex, 1);
|
|
joinedMUCs.splice(joinedMUCs.indexOf(roomName), 1);
|
|
}
|
|
|
|
ws.send(XMLBuilder.create("presence")
|
|
.attribute("to", jid)
|
|
.attribute("from", getMUCmember(roomName, displayName, accountId, resource))
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("type", "unavailable")
|
|
.element("x").attribute("xmlns", "http://jabber.org/protocol/muc#user")
|
|
.element("item")
|
|
.attribute("nick", getMUCmember(roomName, displayName, accountId, resource).replace(`${roomName}@muc.${global.xmppDomain}/`, ""))
|
|
.attribute("jid", jid)
|
|
.attribute("role", "none").up()
|
|
.element("status").attribute("code", "110").up()
|
|
.element("status").attribute("code", "100").up()
|
|
.element("status").attribute("code", "170").up().up().toString());
|
|
return;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (msg.root.children.find(i => i.name == "muc:x") || msg.root.children.find(i => i.name == "x")) {
|
|
if (!msg.root.attributes.to) return;
|
|
|
|
let roomName = msg.root.attributes.to.split("@")[0];
|
|
|
|
if (!global.MUCs[roomName]) global.MUCs[roomName] = { members: [] };
|
|
|
|
if (global.MUCs[roomName].members.find(i => i.accountId == accountId)) return;
|
|
|
|
global.MUCs[roomName].members.push({ accountId: accountId });
|
|
|
|
joinedMUCs.push(roomName);
|
|
|
|
ws.send(XMLBuilder.create("presence")
|
|
.attribute("to", jid)
|
|
.attribute("from", getMUCmember(roomName, displayName, accountId, resource))
|
|
.attribute("xmlns", "jabber:client")
|
|
.element("x").attribute("xmlns", "http://jabber.org/protocol/muc#user")
|
|
.element("item")
|
|
.attribute("nick", getMUCmember(roomName, displayName, accountId, resource).replace(`${roomName}@muc.${global.xmppDomain}/`, ""))
|
|
.attribute("jid", jid)
|
|
.attribute("role", "participant")
|
|
.attribute("affiliation", "none").up()
|
|
.element("status").attribute("code", "110").up()
|
|
.element("status").attribute("code", "100").up()
|
|
.element("status").attribute("code", "170").up()
|
|
.element("status").attribute("code", "201").up().up().toString());
|
|
|
|
global.MUCs[roomName].members.forEach(member => {
|
|
let ClientData = global.Clients.find(i => i.accountId == member.accountId);
|
|
if (!ClientData) return;
|
|
|
|
ws.send(XMLBuilder.create("presence")
|
|
.attribute("from", getMUCmember(roomName, ClientData.displayName, ClientData.accountId, ClientData.resource))
|
|
.attribute("to", jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.element("x")
|
|
.attribute("xmlns", "http://jabber.org/protocol/muc#user")
|
|
.element("item")
|
|
.attribute("nick", getMUCmember(roomName, ClientData.displayName, ClientData.accountId, ClientData.resource).replace(`${roomName}@muc.${global.xmppDomain}/`, ""))
|
|
.attribute("jid", ClientData.jid)
|
|
.attribute("role", "participant")
|
|
.attribute("affiliation", "none").up().up().toString());
|
|
|
|
if (accountId == ClientData.accountId) return;
|
|
|
|
ClientData.client.send(XMLBuilder.create("presence")
|
|
.attribute("from", getMUCmember(roomName, displayName, accountId, resource))
|
|
.attribute("to", ClientData.jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.element("x")
|
|
.attribute("xmlns", "http://jabber.org/protocol/muc#user")
|
|
.element("item")
|
|
.attribute("nick", getMUCmember(roomName, displayName, accountId, resource).replace(`${roomName}@muc.${global.xmppDomain}/`, ""))
|
|
.attribute("jid", jid)
|
|
.attribute("role", "participant")
|
|
.attribute("affiliation", "none").up().up().toString());
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
let findStatus = msg.root.children.find(i => i.name == "status");
|
|
|
|
if (!findStatus || !findStatus.content) return;
|
|
if (!isJSON(findStatus.content)) return;
|
|
if (Array.isArray(JSON.parse(findStatus.content))) return;
|
|
|
|
let status = findStatus.content;
|
|
let away = msg.root.children.find(i => i.name == "show") ? true : false;
|
|
|
|
await updatePresenceForFriends(ws, status, away, false);
|
|
functions.getPresenceFromUser(accountId, accountId, false);
|
|
break;
|
|
}
|
|
|
|
if (!clientExists && !connectionClosed) {
|
|
if (accountId && displayName && token && jid && ID && resource && Authenticated) {
|
|
global.Clients.push({
|
|
client: ws,
|
|
accountId: accountId,
|
|
displayName: displayName,
|
|
token: token,
|
|
jid: jid,
|
|
resource: resource,
|
|
lastPresenceUpdate: {
|
|
away: false,
|
|
status: "{}"
|
|
}
|
|
});
|
|
|
|
clientExists = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on('close', () => { connectionClosed = true; clientExists = false; RemoveClient(ws, joinedMUCs); });
|
|
});
|
|
|
|
function Error(ws) {
|
|
ws.send(XMLBuilder.create("close").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-framing").toString());
|
|
ws.close();
|
|
}
|
|
|
|
function RemoveClient(ws, joinedMUCs) {
|
|
let clientIndex = global.Clients.findIndex(i => i.client == ws);
|
|
let client = global.Clients[clientIndex];
|
|
|
|
if (clientIndex == -1) return;
|
|
|
|
let ClientStatus = JSON.parse(client.lastPresenceUpdate.status);
|
|
|
|
updatePresenceForFriends(ws, "{}", false, true);
|
|
|
|
global.Clients.splice(clientIndex, 1);
|
|
|
|
for (let roomName of joinedMUCs) {
|
|
if (global.MUCs[roomName]) {
|
|
let memberIndex = global.MUCs[roomName].members.findIndex(i => i.accountId == client.accountId);
|
|
|
|
if (memberIndex != -1) global.MUCs[roomName].members.splice(memberIndex, 1);
|
|
}
|
|
}
|
|
|
|
let partyId = "";
|
|
|
|
try {
|
|
switch (true) {
|
|
case (!ClientStatus.Properties): break;
|
|
case (isObject(ClientStatus.Properties)): {
|
|
for (let key in ClientStatus.Properties) {
|
|
if (key.toLowerCase().startsWith("party.joininfo")) {
|
|
if (isObject(ClientStatus.Properties[key])) partyId = ClientStatus.Properties[key].partyId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
if (partyId && typeof partyId == "string") {
|
|
global.Clients.forEach(ClientData => {
|
|
if (client.accountId == ClientData.accountId) return;
|
|
|
|
ClientData.client.send(XMLBuilder.create("message")
|
|
.attribute("id", functions.MakeID().replace(/-/ig, "").toUpperCase())
|
|
.attribute("from", client.jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("to", ClientData.jid)
|
|
.element("body", JSON.stringify({
|
|
"type": "com.epicgames.party.memberexited",
|
|
"payload": {
|
|
"partyId": partyId,
|
|
"memberId": client.accountId,
|
|
"wasKicked": false
|
|
},
|
|
"timestamp": new Date().toISOString()
|
|
})).up().toString());
|
|
});
|
|
}
|
|
|
|
log.xmpp(`An xmpp client with the displayName ${client.displayName} has logged out.`);
|
|
}
|
|
|
|
async function getPresenceFromFriends(ws, accountId, jid) {
|
|
let friends = await Friends.findOne({ accountId: accountId }).lean();
|
|
if (!friends) return;
|
|
|
|
let accepted = friends.list.accepted;
|
|
|
|
accepted.forEach(friend => {
|
|
let ClientData = global.Clients.find(i => i.accountId == friend.accountId);
|
|
if (!ClientData) return;
|
|
|
|
let xml = XMLBuilder.create("presence")
|
|
.attribute("to", jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("from", ClientData.jid)
|
|
.attribute("type", "available")
|
|
|
|
if (ClientData.lastPresenceUpdate.away) xml = xml.element("show", "away").up().element("status", ClientData.lastPresenceUpdate.status).up();
|
|
else xml = xml.element("status", ClientData.lastPresenceUpdate.status).up();
|
|
|
|
ws.send(xml.toString());
|
|
});
|
|
}
|
|
|
|
async function updatePresenceForFriends(ws, body, away, offline) {
|
|
let SenderIndex = global.Clients.findIndex(i => i.client == ws);
|
|
let SenderData = global.Clients[SenderIndex];
|
|
|
|
if (SenderIndex == -1) return;
|
|
|
|
global.Clients[SenderIndex].lastPresenceUpdate.away = away;
|
|
global.Clients[SenderIndex].lastPresenceUpdate.status = body;
|
|
|
|
let friends = await Friends.findOne({ accountId: SenderData.accountId });
|
|
let accepted = friends.list.accepted;
|
|
|
|
accepted.forEach(friend => {
|
|
let ClientData = global.Clients.find(i => i.accountId == friend.accountId);
|
|
if (!ClientData) return;
|
|
|
|
let xml = XMLBuilder.create("presence")
|
|
.attribute("to", ClientData.jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.attribute("from", SenderData.jid)
|
|
.attribute("type", offline ? "unavailable" : "available");
|
|
|
|
if (away) xml = xml.element("show", "away").up().element("status", body).up();
|
|
else xml = xml.element("status", body).up();
|
|
|
|
ClientData.client.send(xml.toString());
|
|
});
|
|
}
|
|
|
|
function sendXmppMessageToClient(senderJid, msg, body) {
|
|
if (typeof body == "object") body = JSON.stringify(body);
|
|
|
|
let receiver = global.Clients.find(i => i.jid.split("/")[0] == msg.root.attributes.to || i.jid == msg.root.attributes.to);
|
|
if (!receiver) return;
|
|
|
|
receiver.client.send(XMLBuilder.create("message")
|
|
.attribute("from", senderJid)
|
|
.attribute("id", msg.root.attributes.id)
|
|
.attribute("to", receiver.jid)
|
|
.attribute("xmlns", "jabber:client")
|
|
.element("body", `${body}`).up().toString());
|
|
}
|
|
|
|
function getMUCmember(roomName, displayName, accountId, resource) {
|
|
return `${roomName}@muc.${global.xmppDomain}/${encodeURI(displayName)}:${accountId}:${resource}`;
|
|
}
|
|
|
|
function isObject(value) {
|
|
if (typeof value == "object" && !Array.isArray(value)) return true;
|
|
else return false;
|
|
}
|
|
|
|
function isJSON(str) {
|
|
try {
|
|
JSON.parse(str)
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|