diff --git a/data/src/scripts/login_logout/login.rs2 b/data/src/scripts/login_logout/login.rs2 index 00462a2f01..8c59a82ba7 100644 --- a/data/src/scripts/login_logout/login.rs2 +++ b/data/src/scripts/login_logout/login.rs2 @@ -16,10 +16,10 @@ softtimer(stat_replenish, 100); softtimer(health_replenish, 100); // random event timer settimer(general_macro_events, 500); -// chest macro gas +// chest macro gas ~check_chest_macro_gas; // for logout out during a general macro event, they respawn when you log back in -// queue: +// queue: // - https://youtu.be/T0ulsgorBkY?list=PLn23LiLYLb1Y3P9S9qZbijcJihiD416jT // - https://youtu.be/HwGAzcmvF9k?list=PLn23LiLYLb1Y3P9S9qZbijcJihiD416jT // npc spawns only after interface is closed diff --git a/src/engine/World.ts b/src/engine/World.ts index 893c33482d..a15856b0ac 100644 --- a/src/engine/World.ts +++ b/src/engine/World.ts @@ -1623,14 +1623,14 @@ class World { return; } - const { username, lowMemory, reconnecting, staffmodlevel, muted_until } = msg; + const { username, password, lowMemory, reconnecting, staffmodlevel, muted_until } = msg; let save = new Uint8Array(); if (reply === 0 || reply === 2) { save = msg.save; } - const player = PlayerLoading.load(username, new Packet(save), client); + const player = PlayerLoading.load(username, password, new Packet(save), client); player.reconnecting = reconnecting; player.staffModLevel = staffmodlevel; player.lowMemory = lowMemory; diff --git a/src/engine/entity/NetworkPlayer.ts b/src/engine/entity/NetworkPlayer.ts index 3d3d535f69..1ad796054e 100644 --- a/src/engine/entity/NetworkPlayer.ts +++ b/src/engine/entity/NetworkPlayer.ts @@ -51,8 +51,8 @@ export class NetworkPlayer extends Player { opcalled: boolean = false; opucalled: boolean = false; - constructor(username: string, username37: bigint, client: ClientSocket) { - super(username, username37); + constructor(username: string, username37: bigint, password: string | null, client: ClientSocket) { + super(username, username37, password); this.client = client; this.client.player = this; diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index c78edd63d8..3a4d29535a 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import Packet from '#/io/Packet.js'; -import {fromBase37, toDisplayName} from '#/util/JString.js'; +import { toDisplayName } from '#/util/JString.js'; import FontType from '#/cache/config/FontType.js'; import Component from '#/cache/config/Component.js'; @@ -135,7 +135,7 @@ export default class Player extends PathingEntity { save() { const sav = Packet.alloc(1); sav.p2(0x2004); // magic - sav.p2(5); // version + sav.p2(6); // version sav.p2(this.x); sav.p2(this.z); @@ -202,8 +202,17 @@ export default class Player extends PathingEntity { sav.p4(this.afkZones[index]); } sav.p2(this.lastAfkZone); + sav.p1((this.publicChat << 4) | (this.privateChat << 2) | this.tradeDuel); + if (this.lastAddress) { + sav.p1(this.lastAddress.length); + sav.pdata(this.lastAddress, 0, this.lastAddress.length); + } else { + sav.p1(0); + } + sav.p8(this.lastDate); + sav.p4(Packet.getcrc(sav.data, 0, sav.pos)); const data = sav.data.subarray(0, sav.pos); sav.release(); @@ -214,6 +223,7 @@ export default class Player extends PathingEntity { username: string; username37: bigint; displayName: string; + password: string | null; // this is not saved anywhere, only used for ciphering the ip address. body: number[] = [ 0, // hair 10, // beard @@ -328,10 +338,14 @@ export default class Player extends PathingEntity { muted_until: Date | null = null; - constructor(username: string, username37: bigint) { + lastAddress: Uint8Array | null = null; + lastDate: bigint = 0n; + + constructor(username: string, username37: bigint, password: string | null) { super(0, 3094, 3106, 1, 1, EntityLifeCycle.FOREVER, MoveRestrict.NORMAL, BlockWalk.NPC, MoveStrategy.SMART, InfoProt.PLAYER_FACE_COORD.id, InfoProt.PLAYER_FACE_ENTITY.id); // tutorial island. this.username = username; this.username37 = username37; + this.password = password; this.displayName = toDisplayName(username); this.vars = new Int32Array(VarPlayerType.count); this.varsString = new Array(VarPlayerType.count); diff --git a/src/engine/entity/PlayerLoading.ts b/src/engine/entity/PlayerLoading.ts index 207fe7c5d6..82ad1a04fa 100644 --- a/src/engine/entity/PlayerLoading.ts +++ b/src/engine/entity/PlayerLoading.ts @@ -12,7 +12,6 @@ import { NetworkPlayer } from '#/engine/entity/NetworkPlayer.js'; import Player, { getExpByLevel, getLevelByExp } from '#/engine/entity/Player.js'; import PlayerStat from '#/engine/entity/PlayerStat.js'; -import Environment from '#/util/Environment.js'; import InvType from '#/cache/config/InvType.js'; export class PlayerLoading { @@ -32,16 +31,16 @@ export class PlayerLoading { save = new Packet(new Uint8Array()); } - return PlayerLoading.load(name, save, null); + return PlayerLoading.load(name, null, save, null); } - static load(name: string, sav: Packet, client: ClientSocket | null) { + static load(name: string, password: string | null, sav: Packet, client: ClientSocket | null) { const name37 = toBase37(name); const safeName = fromBase37(name37); const player = client - ? new NetworkPlayer(safeName, name37, client) - : new Player(safeName, name37); + ? new NetworkPlayer(safeName, name37, password, client) + : new Player(safeName, name37, password); if (sav.data.length < 2) { for (let i = 0; i < 21; i++) { @@ -62,7 +61,7 @@ export class PlayerLoading { } const version = sav.g2(); - if (version > 5) { + if (version > 6) { throw new Error('Unsupported player save format'); } @@ -150,6 +149,17 @@ export class PlayerLoading { player.tradeDuel = packedChatModes & 0b11; } + // last login info + if (version >= 6) { + const length = sav.g1(); + if (length > 0) { + const lastAddress = new Uint8Array(length); + sav.gdata(lastAddress, 0, lastAddress.length); + player.lastAddress = lastAddress; + } + player.lastDate = sav.g8(); + } + player.combatLevel = player.getCombatLevel(); player.lastResponse = World.currentTick; diff --git a/src/engine/script/handlers/PlayerOps.ts b/src/engine/script/handlers/PlayerOps.ts index ee8dd263f9..a06a57ffa8 100644 --- a/src/engine/script/handlers/PlayerOps.ts +++ b/src/engine/script/handlers/PlayerOps.ts @@ -40,6 +40,8 @@ import IfSetText from '#/network/server/model/IfSetText.js'; import IfSetNpcHead from '#/network/server/model/IfSetNpcHead.js'; import IfSetPosition from '#/network/server/model/IfSetPosition.js'; +import ClientSocket from '#/server/ClientSocket.js'; + import ColorConversion from '#/util/ColorConversion.js'; import {findPath} from '#/engine/GameMap.js'; @@ -61,6 +63,7 @@ import { SkinColourValid } from '#/engine/script/ScriptValidators.js'; import LoggerEventType from '#/server/logger/LoggerEventType.js'; +import { decrypt, encrypt } from '#/util/Crypto.js'; const PlayerOps: CommandHandlers = { [ScriptOpcode.FINDUID]: state => { @@ -801,18 +804,32 @@ const PlayerOps: CommandHandlers = { return; } - const client = player.client; + const client: ClientSocket = player.client; + const password: string | null = player.password; + const remoteAddress: string | null = client.remoteAddress !== ClientSocket.NO_ADDRESS ? client.remoteAddress : null; - const remoteAddress = client.remoteAddress; - if (remoteAddress == null) { - return; - } + // if decrypt throws then the interface just won't even show on the client side + const lastAddress: string | null = player.lastAddress && password ? decrypt(player.lastAddress, password) : remoteAddress; + const lastDate: bigint = player.lastDate === 0n ? BigInt(Date.now()) : player.lastDate; - const lastLoginIp = new Uint32Array(new Uint8Array(remoteAddress.split('.').map(x => parseInt(x))).reverse().buffer)[0]; + const nextDate: bigint = BigInt(Date.now()); - // 201 sends welcome_screen if. - // not 201 sends welcome_screen_warning if. - player.lastLoginInfo(lastLoginIp, 0, 201, 0); + // message centre/recovery + const unreadMessageCount: number = 0; + const hasRecovery: boolean = true; + const daysSinceRecoveryChange: number = hasRecovery ? 201 : 0; + + player.lastLoginInfo( + lastAddress ? new Uint32Array(new Uint8Array(lastAddress.split('.').map(Number)).reverse().buffer)[0] : 0, + Number(nextDate - lastDate) / (1000 * 60 * 60 * 24), + daysSinceRecoveryChange, + unreadMessageCount, + ); + + if (remoteAddress && password) { + player.lastAddress = encrypt(remoteAddress, password); + } + player.lastDate = nextDate; }, [ScriptOpcode.BAS_READYANIM]: state => { diff --git a/src/network/rs225/client/handler/ClientCheatHandler.ts b/src/network/rs225/client/handler/ClientCheatHandler.ts index e448e55400..3c842ec136 100644 --- a/src/network/rs225/client/handler/ClientCheatHandler.ts +++ b/src/network/rs225/client/handler/ClientCheatHandler.ts @@ -62,13 +62,13 @@ export default class ClientCheatHandler extends MessageHandler { } else if (cmd === 'bots') { player.messageGame('Adding bots'); for (let i = 0; i < 1999; i++) { - const bot: Player = PlayerLoading.load(`bot${i}`, new Packet(new Uint8Array()), new NullClientSocket()); + const bot: Player = PlayerLoading.load(`bot${i}`, null, new Packet(new Uint8Array()), new NullClientSocket()); World.addPlayer(bot); } } else if (cmd === 'lightbots') { player.messageGame('Adding lightweight bots'); for (let i = 0; i < 1999; i++) { - const bot: Player = PlayerLoading.load(`bot${i}`, new Packet(new Uint8Array()), null); + const bot: Player = PlayerLoading.load(`bot${i}`, null, new Packet(new Uint8Array()), null); World.addPlayer(bot); } } else if (cmd === 'teleall') { diff --git a/src/server/ClientSocket.ts b/src/server/ClientSocket.ts index 5edc17bdd9..b12def8e0b 100644 --- a/src/server/ClientSocket.ts +++ b/src/server/ClientSocket.ts @@ -6,8 +6,10 @@ import Packet from '#/io/Packet.js'; import { NetworkPlayer } from '#/engine/entity/NetworkPlayer.js'; export default abstract class ClientSocket { + public static readonly NO_ADDRESS: string = 'unknown'; + uuid = randomUUID(); - remoteAddress = 'unknown'; + remoteAddress = ClientSocket.NO_ADDRESS; totalBytesRead = 0; totalBytesWritten = 0; diff --git a/src/server/login/LoginThread.ts b/src/server/login/LoginThread.ts index 929331880b..2db2f6a629 100644 --- a/src/server/login/LoginThread.ts +++ b/src/server/login/LoginThread.ts @@ -58,6 +58,7 @@ async function handleRequests(parentPort: ParentPort, msg: any) { type: 'player_login', socket, username, + password, lowMemory, reconnecting, ...await client.playerLogin(username, password, uid) @@ -75,6 +76,7 @@ async function handleRequests(parentPort: ParentPort, msg: any) { type: 'player_login', socket, username, + password, lowMemory, reconnecting, reply: 4, @@ -86,6 +88,7 @@ async function handleRequests(parentPort: ParentPort, msg: any) { type: 'player_login', socket, username, + password, lowMemory, reconnecting, reply: 0, diff --git a/src/util/Crypto.ts b/src/util/Crypto.ts new file mode 100644 index 0000000000..418a526baf --- /dev/null +++ b/src/util/Crypto.ts @@ -0,0 +1,25 @@ +import crypto from 'crypto'; + +export function encrypt(value: string, passphrase: string): Uint8Array | null { + try { + const iv = crypto.randomBytes(16); + const key = crypto.createHash('sha256').update(passphrase).digest(); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + return new Uint8Array(Buffer.concat([iv, Buffer.concat([cipher.update(value), cipher.final()])])); + } catch (err) { + console.error(err); + } + return null; +} + +export function decrypt(bytes: Uint8Array, passphrase: string): string | null { + try { + const iv = bytes.subarray(0, 16); + const key = crypto.createHash('sha256').update(passphrase).digest(); + const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + return Buffer.concat([cipher.update(bytes.subarray(16)), cipher.final()]).toString(); + } catch (err) { + console.error(err); + } + return null; +}