diff --git a/src/api/acts/login.ts b/src/api/acts/login.ts index 770bbb3..68bb355 100644 --- a/src/api/acts/login.ts +++ b/src/api/acts/login.ts @@ -1,8 +1,8 @@ import { select, count, alias, insert, remove, le, update, minus, eq, and } from "dblang"; -import { outbagURLfromTag } from "../../server/outbagURL.js"; +import { checkSelfTag, outbagServer, outbagURLfromTag } from "../../server/outbagURL.js"; import { PERMISSIONS } from "../../server/permissions.js"; import { getRemote } from "../../server/serverCerts.js"; -import { localhostTag, oConf } from "../../sys/config.js"; +import { oConf } from "../../sys/config.js"; import { sha256, verify } from "../../sys/crypto.js"; import { accounts, db, signupOTA as signupOTATable } from "../../sys/db.js"; import { get64, uts } from "../../sys/tools.js"; @@ -27,9 +27,14 @@ export const signup: Act = { right: 0, data: { name: "name-100", + server: "string", accountKey: "string" }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!checkSelfTag(data.server)) { + aws("error", "data"); + return; + } let countAlias = alias(count(accounts.accID), "countAlias") as any; let query = await select([countAlias], accounts) .query(db); @@ -50,7 +55,7 @@ export const signup: Act = { client.state = STATE.client; client.accID = accID; client.name = data.name; - client.server = localhostTag; + client.server = new outbagServer(data.server, oConf.get("System", "URL") + "", oConf.get("System", "PATHexposed") + "", oConf.get("System", "PORTexposed") + ""); } } else { client.suspect(); @@ -64,10 +69,15 @@ export const signupOTA = { right: 0, data: { name: "string-100", + server: "string", accountKey: "string", OTA: "string" }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!checkSelfTag(data.server)) { + aws("error", "data"); + return; + } // TODO: make transaktion when posible await remove(signupOTATable) .where(le(signupOTATable.expires, uts())) @@ -92,7 +102,7 @@ export const signupOTA = { client.state = STATE.client; client.accID = accID; client.name = data.name; - client.server = localhostTag; + client.server = new outbagServer(data.server, oConf.get("System", "URL") + "", oConf.get("System", "PATHexposed") + "", oConf.get("System", "PORTexposed") + ""); } } else { client.suspect(); @@ -106,9 +116,14 @@ export const signin = { right: 0, data: { name: "string", + server: "string", accountKey: "string" }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!checkSelfTag(data.server)) { + aws("error", "data"); + return; + } let query = await select([accounts.accID, accounts.accountKey, accounts.accountKeySalt], accounts) .where(and( eq(accounts.name, data.name), @@ -126,7 +141,7 @@ export const signin = { client.state = STATE.client; client.accID = accID; client.name = data.name; - client.server = localhostTag; + client.server = new outbagServer(data.server, oConf.get("System", "URL") + "", oConf.get("System", "PATHexposed") + "", oConf.get("System", "PORTexposed") + ""); } } }; diff --git a/src/api/acts/rooms.ts b/src/api/acts/rooms.ts new file mode 100644 index 0000000..d5b6d92 --- /dev/null +++ b/src/api/acts/rooms.ts @@ -0,0 +1,372 @@ +import { alias, and, eq, exists, innerJoinOn, insert, le, minus, naturalJoin, not, or, remove, select, update } from "dblang"; +import { ROOM_RIGHTS } from "../../server/permissions.js"; +import { accounts, db, roomMembers, roomOTAs, rooms } from "../../sys/db.js"; +import { uts } from "../../sys/tools.js"; +import { isRoomFull } from "../helper.js"; +import { Act, Client, STATE } from "../user.js"; + +export const listLocalRooms: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: {}, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let ownerAlias = alias( + select([accounts.name], accounts) + .where(eq(accounts.accID, rooms.owner)), + "owner") as any; + let req = await select([ + rooms.name, + ownerAlias, + rooms.rights, + rooms.public, + rooms.title, + rooms.description, + rooms.icon + ], naturalJoin(rooms, roomMembers)) + .where(and( + eq(roomMembers.name, client.name), + eq(roomMembers.server, client.state == STATE.client ? "local" : client.server.tag) + )) + .query(db); + let out = req.map(d => { + let name = d[rooms.name]; + let owner = d[ownerAlias]; + let rights = d[rooms.rights]; + let isPublic = d[rooms.public]; + let title = d[rooms.title]; + let description = d[rooms.description]; + let icon = d[rooms.icon]; + if (name != null && owner != null && rights != null && isPublic != null && title != null && description != null && icon != null) { + return { name, owner, rights, public: isPublic, title, description, icon }; + } + return null; + }); + aws("ok", out.filter(d => d != null)); + } +}; + +export const getRoomMembers: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "name", + server: "string", // Unused at the Moment + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isInRoom(data.room); + if (roomID == -1) { + aws("error", "existence"); + return; + } + let req = await select([ + roomMembers.name, + roomMembers.server, + roomMembers.admin + ], roomMembers) + .where(eq(rooms.roomID, roomID)) + .query(db); + let out = req.map(d => { + let name = d[roomMembers.name]; + let server = d[roomMembers.server]; + let admin = d[roomMembers.admin]; + if (name != null && server != null && admin != null) { + return { name, server, admin }; + } + return null; + }); + aws("ok", out.filter(d => d != null)); + } +}; + +export const joinRoom: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + OTA: "string" + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let query = await select([rooms.roomID], rooms) + .where(eq(rooms.name, data.room)) + .query(db); + let roomID = (query[0] ?? {})[rooms.roomID]; + if (typeof roomID != "number" || roomID < 0) { + return void aws("error", "ota"); + } + if (await isRoomFull(roomID)) return void aws("error", "limit"); + // TODO: Make Transaktion when possible + await remove(roomOTAs) + .where(le(roomOTAs.expires, uts())) + .query(db); + let req = await update(roomOTAs) + .set(roomOTAs.usesLeft, minus(roomOTAs.usesLeft, 1)) + .query(db); + await remove(roomOTAs) + .where(eq(roomOTAs.usesLeft, 0)) + .query(db); + if (req[1].affectedRows == 0) { + return void aws("error", "ota"); + } + let queryx = await insert( + roomMembers.roomID, + roomMembers.name, + roomMembers.server, + roomMembers.admin + ).add(roomID, client.name, client.server.tag, 0) + .query(db); + if (queryx.affectedRows > 0) { + aws("ok", ""); + } else { + aws("error", "server"); + } + } +}; + +export const joinPublicRoom: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let query = await select([rooms.roomID, rooms.public], rooms) + .where(eq(rooms.name, data.room)) + .query(db); + let roomID = (query[0] ?? {})[rooms.roomID]; + let isPublic = (query[0] ?? {})[rooms.public]; + if (typeof roomID != "number" || roomID < 0 || typeof isPublic != "number") { + return void aws("error", "existence"); + } + if (((client.state == STATE.client) && (isPublic < 1)) || ((client.state == STATE.remote) && (isPublic < 2))) { + return void aws("error", "existence"); + } + if (await isRoomFull(roomID)) return void aws("error", "limit"); + let queryx = await insert( + roomMembers.roomID, + roomMembers.name, + roomMembers.server, + roomMembers.admin + ).add(roomID, client.name, client.server.tag, 0) + .query(db); + if (queryx.affectedRows > 0) { + aws("ok", ""); + } else { + aws("error", "server"); + } + } +}; + +export const getRoomOTAs: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); + if (roomID == -1) return void aws("error", "roomAdmin"); + let req = await select([roomOTAs.token, roomOTAs.expires, roomOTAs.usesLeft], roomOTAs) + .where(eq(roomOTAs.roomID, roomID)) + .query(db); + aws("ok", req.map(d => ({ + token: d[roomOTAs.token], + expires: d[roomOTAs.expires], + usesLeft: d[roomOTAs.usesLeft] + }))); + } +}; + +export const addRoomOTA: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + token: "string-255", + expires: "number", + usesLeft: "number", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); + if (roomID == -1) return void aws("error", "roomAdmin"); + try { + await insert(roomOTAs.roomID, roomOTAs.token, roomOTAs.expires, roomOTAs.usesLeft) + .add(roomID, data.token, data.expires, data.usesLeft) + .query(db); + } catch (error) { + await update(roomOTAs) + .set(roomOTAs.expires, data.expires) + .set(roomOTAs.usesLeft, data.usesLeft) + .where(and( + eq(roomOTAs.token, data.token), + eq(roomOTAs.roomID, roomID) + )).query(db); + } + aws("ok", ""); + } +}; + +export const deleteRoomOTA: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + token: "string" + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); + if (roomID == -1) return void aws("error", "roomAdmin"); + await remove(roomOTAs) + .where(and( + eq(roomOTAs.roomID, roomID), + eq(roomOTAs.token, data.token) + )).query(db); + aws("ok", ""); + } +}; + +export const kickMember: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + roomServer: "string", // Unused at the Moment + name: "string", + server: "string", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.MANAGE_MEMBERS); + if (roomID == -1) return void aws("error", "roomAdmin"); + let req = await remove(roomMembers) + .where(and( + eq(roomMembers.roomID, roomID), + eq(roomMembers.name, data.name), + eq(roomMembers.server, data.server), + or( + not(eq(roomMembers.server, "local")), + not(exists( + select([accounts.accID], innerJoinOn(accounts, rooms, eq(accounts.accID, rooms.owner))) + .where(and( + eq(rooms.roomID, roomMembers.roomID), + eq(accounts.name, roomMembers.name) + )) + )) + ) + )).query(db); + if (req.affectedRows > 0) { + aws("ok", ""); + } else { + aws("error", "existence") + } + } +}; + +export const setAdminStatus: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + roomServer: "string", // Unused at the Moment + name: "string", + server: "string", + admin: "boolean", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.MANAGE_MEMBERS); + if (roomID == -1) return void aws("error", "roomAdmin"); + let req = await update(roomMembers) + .set(roomMembers.admin, data.admin) + .where(and( + eq(roomMembers.roomID, roomID), + eq(roomMembers.name, data.name), + eq(roomMembers.server, data.server) + )).query(db); + if (req.affectedRows > 0) { + aws("ok", ""); + } else { + aws("error", "existence"); + } + } +}; + +export const leaveRoom: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + let roomID = await client.isInRoom(data.room); + if (roomID == -1) return void aws("error", "existence"); + let req = await remove(roomMembers) + .where(and( + eq(roomMembers.roomID, roomID), + eq(roomMembers.name, client.name), + eq(roomMembers.server, client.state == STATE.client ? "local" : client.server.tag), + or( + not(eq(roomMembers.server, "local")), + not(exists( + select([accounts.accID], innerJoinOn(accounts, rooms, eq(accounts.accID, rooms.owner))) + .where(and( + eq(rooms.roomID, roomMembers.roomID), + eq(accounts.name, roomMembers.name) + )) + )) + ) + )).query(db); + if (req.affectedRows > 0) { + aws("ok", ""); + } else { + aws("error", "owner"); + } + } +}; + +export const roomSettings: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", // Unused at the Moment + rights: "number", //see permissions.ts + isPublic: "number" //0 is not, 1 only to clients, 2 or bigger everywhere + }, + func: async (client, data, aws) => { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.CHANGE_SETTINGS); + if (roomID == -1) return void aws("error", "roomAdmin"); + let req = await update(rooms) + .set(rooms.rights, data.rights) + .set(rooms.public, data.isPublic) + .where(eq(rooms.roomID, roomID)) + .query(db); + aws("ok", ""); + } +}; + +export const roomMeta: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "string", + server: "string", + title: "string-255", + description: "string-255", + icon: "string-255" + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) =>{ + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.CHANGE_META); + if (roomID == -1) return void aws("error", "roomAdmin"); + let req = await update(rooms) + .set(rooms.title, data.title) + .set(rooms.description, data.description) + .set(rooms.icon, data.icon) + .where(eq(rooms.roomID, roomID)) + .query(db); + aws("ok", ""); + } +} \ No newline at end of file diff --git a/src/api/helper.ts b/src/api/helper.ts index e69de29..fb29bda 100644 --- a/src/api/helper.ts +++ b/src/api/helper.ts @@ -0,0 +1,19 @@ +import { alias, eq, count, select, naturalJoin, innerJoinOn } from "dblang"; +import { accounts, db, roomMembers, rooms } from "../sys/db.js"; + +export const isRoomFull = async (roomID: number) => { + let currentCount = alias( + select([count(roomMembers.roomMemberID)], roomMembers) + .where(eq(roomMembers.roomID, roomID)), + "currentCount" + ) as any; + let maxCount = alias( + select([accounts.maxUsersPerRoom], + innerJoinOn(accounts, rooms, eq(accounts.accID, rooms.owner))) + .where(eq(rooms.roomID, roomID)), + "maxCount" + ) as any; + let req = await select([currentCount, maxCount], null) + .query(db); + return req[0][currentCount] >= req[0][maxCount]; +}; \ No newline at end of file diff --git a/src/api/post.ts b/src/api/post.ts index 20820ee..f891bd8 100644 --- a/src/api/post.ts +++ b/src/api/post.ts @@ -10,7 +10,8 @@ import { sha256 } from "../sys/crypto.js"; import { get64, uts } from "../sys/tools.js"; import { addShutdownTask } from "nman"; import { suspectRequest } from "../sys/bruteforce.js"; -import { localhostTag } from "../sys/config.js"; +import { checkSelfTag, outbagServer } from "../server/outbagURL.js"; +import { oConf, selfTag } from "../sys/config.js"; let acts = importActs as { [key: string]: Act }; @@ -47,10 +48,15 @@ export const addPostMethods = (server: express.Express) => { aws("error", "token"); return; } - } else if (auth?.params?.name != null && auth?.params?.accountKey != null && typeof auth?.params?.name == "string" && typeof auth?.params?.accountKey == "string") { + } else if (auth?.params?.name != null && auth?.params?.server != null && auth?.params?.accountKey != null && typeof auth?.params?.name == "string" && auth?.params?.server == "string" && typeof auth?.params?.accountKey == "string") { client = new postClient(req.ip); client.client.name = auth?.params?.name; - client.client.server = localhostTag; + let serverTag = auth?.params?.server; + if (!checkSelfTag(serverTag)) { + aws("error", "data"); + return; + } + client.client.server = new outbagServer(serverTag, selfTag.host, selfTag.path, selfTag.port);; let accountKey = auth?.params?.accountKey; let query = await select([accounts.accID, accounts.accountKey, accounts.accountKeySalt], accounts) diff --git a/src/main.ts b/src/main.ts index a61cd15..65da381 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,7 +101,7 @@ async function complete_loaded() { startUpdateCert(); await wait(500); let succ = await generateTag(); - if(!succ) error("Outbag", "Could not check own Server Tag. Remote-Auth will not work! Check if the Server is reachable and the config ist correct!"); + if(!succ) error("Outbag", "Could not check own Server Tag. Remote-Auth may not work! Check if the Server is reachable and the config ist correct!"); activatePost(); activateWS(); log("Server", 'Listening...'); diff --git a/src/server/outbagURL.ts b/src/server/outbagURL.ts index 4cd6c4c..23a779a 100644 --- a/src/server/outbagURL.ts +++ b/src/server/outbagURL.ts @@ -1,3 +1,5 @@ +import { oConf } from "../sys/config.js"; + const WELL_KNOWN_PATH = "/.well-known/outbag/server"; const DEFAULT_PORT = "7223"; @@ -53,3 +55,7 @@ export class outbagServer { return `wss://${this.host}:${this.port}${this.path}`; } }; + +export const checkSelfTag = (tag: string) => { + return tag == oConf.get("System", "URL") || tag == oConf.get("System", "URL") + ":" + oConf.get("System", "PORTexposed") +} \ No newline at end of file diff --git a/src/sys/config.ts b/src/sys/config.ts index 5a7f7fe..381a2a4 100644 --- a/src/sys/config.ts +++ b/src/sys/config.ts @@ -1,60 +1,70 @@ import juml from "juml"; import { outbagServer, outbagURLfromTag } from "../server/outbagURL.js"; +import { debug } from "./log.js"; const conf_struct = { System: { - PORT: { type: "number", default: 7223, env: "OUTBAG_PORT", comment:"The Server will listen on this Port!" }, - PORTexposed: { type: "number", default: 7223, env: "OUTBAG_EXPOSED_PORT" }, - PATHexposed: { type: "string", default: "/", env: "OUTBAG_EXPOSED_PATH" }, - URL: { type: "string", default: "localhost", env: "OUTBAG_HOST" }, - CertLiveSec: { type: "number", default: 60*60*24*30, env: "OUTBAG_CERT_LIVE_SEC" }, + PORT: { type: "number", default: 7223, env: "OUTBAG_PORT", comment: "The Server will listen on this Port!" }, + PORTexposed: { type: "number", default: 7223, env: "OUTBAG_EXPOSED_PORT" }, + PATHexposed: { type: "string", default: "/", env: "OUTBAG_EXPOSED_PATH" }, + URL: { type: "string", default: "localhost", env: "OUTBAG_HOST" }, + CertLiveSec: { type: "number", default: 60 * 60 * 24 * 30, env: "OUTBAG_CERT_LIVE_SEC" }, }, ssl: { - enable: { type: "boolean", default: false, env: "OUTBAG_SSL_ENABLED" }, - privkey: { type: "string", default: "privkey.pem", env: "OUTBAG_SSL_PRIVATE_KEY" }, - cert: { type: "string", default: "cert.pem", env: "OUTBAG_SSL_CERT" }, - chain: { type: "string", default: "chain.pem", env: "OUTBAG_SSL_CHAIN" } + enable: { type: "boolean", default: false, env: "OUTBAG_SSL_ENABLED" }, + privkey: { type: "string", default: "privkey.pem", env: "OUTBAG_SSL_PRIVATE_KEY" }, + cert: { type: "string", default: "cert.pem", env: "OUTBAG_SSL_CERT" }, + chain: { type: "string", default: "chain.pem", env: "OUTBAG_SSL_CHAIN" } }, Database: { - host: { type: "string", default: "localhost", env: "OUTBAG_MYSQL_HOST" }, - port: { type: "number", default: 3306, env: "OUTBAG_MYSQL_PORT" }, - user: { type: "string", default: "admin", env: "OUTBAG_MYSQL_USER" }, - password: { type: "string", default: "", env: "OUTBAG_MYSQL_PASSWORD" }, - database: { type: "string", default: "outbag", env: "OUTBAG_MYSQL_DATABASE" } + host: { type: "string", default: "localhost", env: "OUTBAG_MYSQL_HOST" }, + port: { type: "number", default: 3306, env: "OUTBAG_MYSQL_PORT" }, + user: { type: "string", default: "admin", env: "OUTBAG_MYSQL_USER" }, + password: { type: "string", default: "", env: "OUTBAG_MYSQL_PASSWORD" }, + database: { type: "string", default: "outbag", env: "OUTBAG_MYSQL_DATABASE" } }, Settings: { - maxUsers: { type:"number", default:0, env: "OUTBAG_MAX_USERS"},//Infinity = -1 - defaultMaxRooms: { type:"number", default:3, env: "OUTBAG_DEFAULT_MAX_ROOMS"},//Infinity = -1 - defaultMaxRoomSize: { type:"number", default:10000, env: "OUTBAG_DEFAULT_MAX_ROOMS_SIZE"},//Infinity = -1 - defaultMaxUsersPerRoom: { type:"number", default:5, env: "OUTBAG_DEFAULT_MAX_USERS_PER_ROOM"},//Infinity = -1 + maxUsers: { type: "number", default: 0, env: "OUTBAG_MAX_USERS" },//Infinity = -1 + defaultMaxRooms: { type: "number", default: 3, env: "OUTBAG_DEFAULT_MAX_ROOMS" },//Infinity = -1 + defaultMaxRoomSize: { type: "number", default: 10000, env: "OUTBAG_DEFAULT_MAX_ROOMS_SIZE" },//Infinity = -1 + defaultMaxUsersPerRoom: { type: "number", default: 5, env: "OUTBAG_DEFAULT_MAX_USERS_PER_ROOM" },//Infinity = -1 } }; export const oConf = new juml(conf_struct); -export let localhostTag: outbagServer = new outbagServer( - "localhost", - oConf.get("System", "URL")+"", - oConf.get("System", "PATHexposed")+"", - oConf.get("System", "PORTexposed")+"", +export let selfTag: outbagServer = new outbagServer( + "localhost", + oConf.get("System", "URL") + "", + oConf.get("System", "PATHexposed") + "", + oConf.get("System", "PORTexposed") + "", ); -export let selfTag: outbagServer = localhostTag; - export const generateTag = async () => { try { + selfTag = new outbagServer( + "localhost", + oConf.get("System", "URL") + "", + oConf.get("System", "PATHexposed") + "", + oConf.get("System", "PORTexposed") + "", + ); + let initselfTag = selfTag; let mainServerHost: outbagServer | null = null; try { - mainServerHost = await outbagURLfromTag(oConf.get("System", "URL")); - } catch (error) {} + mainServerHost = await outbagURLfromTag(oConf.get("System", "URL")); + } catch (error) { } let serverHostPort = await outbagURLfromTag( oConf.get("System", "URL") + ":" + oConf.get("System", "PORTexposed")); - if(mainServerHost == null || mainServerHost.tag != serverHostPort.tag){ + if (mainServerHost == null || mainServerHost.tag != serverHostPort.tag) { selfTag = serverHostPort; - }else selfTag = mainServerHost; + } else selfTag = mainServerHost; + if (initselfTag.httpsURL != selfTag.httpsURL) { + console.log(initselfTag, initselfTag.httpsURL, selfTag.httpsURL); + debug("Outbag", "Not matching Server host, port, path and expected server link."); + return false; + } return true; } catch (error) { return false; } - }; \ No newline at end of file