stuff
This commit is contained in:
3
server/.prettierignore
Normal file
3
server/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
build
|
||||||
|
coverage
|
||||||
1
server/.prettierrc
Normal file
1
server/.prettierrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -11,52 +11,21 @@ export class Character {
|
|||||||
/** @type {string} character's name */
|
/** @type {string} character's name */
|
||||||
name;
|
name;
|
||||||
|
|
||||||
/**
|
/** @protected @type {number} The number of XP the character has. */
|
||||||
* Alive?
|
|
||||||
*
|
|
||||||
* @protected
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
_alive = true;
|
|
||||||
get alive() {
|
|
||||||
return _alive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
* @type {number} The number of XP the character has.
|
|
||||||
*/
|
|
||||||
_xp = 0;
|
_xp = 0;
|
||||||
get xp() {
|
get xp() { return this._xp; }
|
||||||
return this._xp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** @protected @type {number} The character's level. */
|
||||||
* @protected
|
|
||||||
* @type {number} The character's level.
|
|
||||||
*/
|
|
||||||
_level = 1;
|
_level = 1;
|
||||||
get level() {
|
get level() { return this._level; }
|
||||||
return this._level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** @protected @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id */
|
||||||
* @protected
|
|
||||||
* @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id
|
|
||||||
*/
|
|
||||||
_id;
|
_id;
|
||||||
get id() {
|
get id() { return this._id; }
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** @protected @type {string} username of the player that owns this character. */
|
||||||
* @protected
|
|
||||||
* @type {string} username of the player that owns this character.
|
|
||||||
*/
|
|
||||||
_username;
|
_username;
|
||||||
get username() {
|
get username() { return this._username; }
|
||||||
return this._username;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {string} Bloodline background */
|
/** @type {string} Bloodline background */
|
||||||
ancestry;
|
ancestry;
|
||||||
@@ -83,7 +52,7 @@ export class Character {
|
|||||||
equipment = new Map();
|
equipment = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} playerUname The name of player who owns this character. Note that the game can own a character - somehow.
|
* @param {string} username The name of player who owns this character. Note that the game can own a character - somehow.
|
||||||
* @param {string} name The name of the character
|
* @param {string} name The name of the character
|
||||||
* @param {boolean} initialize Should we initialize the character
|
* @param {boolean} initialize Should we initialize the character
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* Serializing this object effectively saves the game.
|
* Serializing this object effectively saves the game.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import WebSocket from "ws";
|
||||||
import { Character } from "./character";
|
import { Character } from "./character";
|
||||||
import { ItemTemplate } from "./item";
|
import { ItemTemplate } from "./item";
|
||||||
|
|
||||||
@@ -28,7 +29,13 @@ class Game{
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @protected
|
* @protected
|
||||||
* @type {Map<string,Player>} The list of users in the game
|
* @type {Map<string,Player>} Map of users in the game username->Player
|
||||||
*/
|
*/
|
||||||
_players = new Map();
|
_playersByName = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {Map<WebSocket,Player>} Map of users in the game username->Player
|
||||||
|
*/
|
||||||
|
_playersBySocket = new Map();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,78 @@
|
|||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player Account.
|
||||||
|
*
|
||||||
|
* 1. Contain persistent player account info.
|
||||||
|
* 2. Contain the connection to the client machine if the player is currently playing the game.
|
||||||
|
* 3. Contain session information.
|
||||||
|
*
|
||||||
|
* We can do this because we only allow a single websocket per player account.
|
||||||
|
* You are not allowed to log in if a connection/socket is already open.
|
||||||
|
*
|
||||||
|
* We regularly ping and pong to ensure that stale connections are closed.
|
||||||
|
*
|
||||||
|
*/
|
||||||
export class Player{
|
export class Player{
|
||||||
|
/** @protected @type {string} unique username */
|
||||||
_username;
|
_username;
|
||||||
|
get username() { return this._username; }
|
||||||
|
|
||||||
|
/** @protected @type {string} */
|
||||||
_passwordHash;
|
_passwordHash;
|
||||||
alias;
|
get passwordHash() { return this._passwordHash; }
|
||||||
|
|
||||||
|
/** @protected @type {WebSocket} Player's current and only websocket. If undefined, the player is not logged in. */
|
||||||
|
_websocket;
|
||||||
|
get websocket() { return this._websocket; }
|
||||||
|
|
||||||
|
/** @protected @type {Date} */
|
||||||
|
_latestSocketReceived;
|
||||||
|
|
||||||
|
constructor(username, passwordHash) {
|
||||||
|
this._username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {WebSocket} websocket */
|
||||||
|
clientConnected(websocket) {
|
||||||
|
this._websocket = websocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Send a message back to the client via the WebSocket.
|
||||||
|
*
|
||||||
|
* @param {string} message
|
||||||
|
* @return {boolean} success
|
||||||
|
*/
|
||||||
|
_send(data) {
|
||||||
|
if (!this._websocket) {
|
||||||
|
console.error("Trying to send a message to an uninitialized websocket", this, data)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this._websocket.readyState === WebSocket.OPEN) {
|
||||||
|
this._websocket.send(JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this._websocket.readyState === WebSocket.CLOSED) {
|
||||||
|
console.error("Trying to send a message through a CLOSED websocket", this, data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this._websocket.readyState === WebSocket.CLOSING) {
|
||||||
|
console.error("Trying to send a message through a CLOSING websocket", this, data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this._websocket.readyState === WebSocket.CONNECTING) {
|
||||||
|
console.error("Trying to send a message through a CONNECTING (not yet open) websocket", this, data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Trying to send a message through a websocket with an UNKNOWN readyState (%d)", this.websocket.readyState, this, data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPrompt() {
|
||||||
|
this.sendMessage(`\n[${this.currentRoom}] > `);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
server/package-lock.json
generated
19
server/package-lock.json
generated
@@ -12,7 +12,8 @@
|
|||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1",
|
||||||
|
"prettier": "3.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -302,6 +303,22 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
|
|||||||
@@ -8,14 +8,20 @@
|
|||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js"
|
"dev": "nodemon server.js"
|
||||||
},
|
},
|
||||||
"keywords": ["mud", "websocket", "game", "multiplayer"],
|
"keywords": [
|
||||||
|
"mud",
|
||||||
|
"websocket",
|
||||||
|
"game",
|
||||||
|
"multiplayer"
|
||||||
|
],
|
||||||
"author": "Your Name",
|
"author": "Your Name",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1",
|
||||||
|
"prettier": "3.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
211
server/server.js
211
server/server.js
@@ -1,194 +1,73 @@
|
|||||||
const WebSocket = require('ws');
|
import WebSocket, { WebSocketServer } from "ws";
|
||||||
const http = require('http');
|
import http from "http";
|
||||||
const path = require('path');
|
import path from "path";
|
||||||
const fs = require('fs');
|
import fs from "fs";
|
||||||
|
import { Player } from "./models/player.js";
|
||||||
|
import { Game } from "./models/game.js";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player
|
* Parse a string with json-encoded data without throwing exceptions.
|
||||||
* @property WebSocket websocket
|
|
||||||
*/
|
|
||||||
class Player {
|
|
||||||
/**
|
|
||||||
*
|
*
|
||||||
* @param {String} name
|
* @param {string} data
|
||||||
* @param {WebSocket} websocket
|
* @return {any}
|
||||||
*/
|
*/
|
||||||
constructor(name, websocket) {
|
function parseJson(data) {
|
||||||
this.name = name;
|
if (typeof data !== "string") {
|
||||||
this.websocket = websocket;
|
console.error("Attempting to parse json, but data was not even a string", data);
|
||||||
this.currentRoom = 'town_square';
|
return;
|
||||||
this.health = 100;
|
|
||||||
this.inventory = [];
|
|
||||||
this.level = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
try {
|
||||||
* Send a message back to the client via the websocket.
|
return JSON.parse(data)
|
||||||
*
|
} catch (error) {
|
||||||
* @param {string} message
|
console.error('Error parsing data as json:', error, data);
|
||||||
*/
|
|
||||||
sendMessage(message) {
|
|
||||||
if (this.websocket.readyState === WebSocket.OPEN) {
|
|
||||||
this.websocket.send(JSON.stringify({
|
|
||||||
type: 'message',
|
|
||||||
content: message
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPrompt() {
|
|
||||||
this.sendMessage(`\n[${this.currentRoom}] > `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Room {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} id
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} description
|
|
||||||
* @param {string[]} exits
|
|
||||||
*/
|
|
||||||
constructor(id, name, description, exits = {}) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.description = description;
|
|
||||||
this.exits = exits; // { north: 'room_id', south: 'room_id' }
|
|
||||||
this.players = new Set();
|
|
||||||
this.items = [];
|
|
||||||
this.npcs = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a player to the list of active players.
|
|
||||||
*
|
|
||||||
* (an active player is a player that currently has an active web socketA)
|
|
||||||
*
|
|
||||||
* @param {Player} player
|
|
||||||
*/
|
|
||||||
addPlayer(player) {
|
|
||||||
this.players.add(player);
|
|
||||||
this.broadcastToRoom(`${player.name} enters the room.`, player);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a player from the list of active players.
|
|
||||||
*
|
|
||||||
* (an active player is a player that currently has an active web socketA)
|
|
||||||
*
|
|
||||||
* @param {Player} player
|
|
||||||
*/
|
|
||||||
removePlayer(player) {
|
|
||||||
this.players.delete(player);
|
|
||||||
this.broadcastToRoom(`${player.name} leaves the room.`, player);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to all other players in this room.
|
|
||||||
*
|
|
||||||
* @param {string} message
|
|
||||||
* @param {Player} excludePlayer A single player to exclude from the broadcast
|
|
||||||
*/
|
|
||||||
broadcastToRoom(message, excludePlayer = null) {
|
|
||||||
// for (const player of this.players) {
|
|
||||||
// if (player !== excludePlayer) {
|
|
||||||
// player.sendMessage(message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
this.getPlayersExcept(excludePlayer).forEach((player) => {
|
|
||||||
player.sendMessage(message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlayersExcept(excludePlayer) {
|
|
||||||
return Array.from(this.players).filter(p => p !== excludePlayer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MudServer {
|
class MudServer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.players = new Map(); // websocket -> Player
|
this.game = new Game();
|
||||||
this.rooms = new Map();
|
|
||||||
this.playersByName = new Map(); // name -> Player
|
|
||||||
this.initializeRooms();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeRooms() {
|
|
||||||
const townSquare = new Room(
|
|
||||||
'town_square',
|
|
||||||
'Town Square',
|
|
||||||
'You are standing in the bustling town square. A fountain sits in the center, and cobblestone paths lead in all directions. The inn lies to the north, and a mysterious forest path leads east.',
|
|
||||||
{ north: 'inn', east: 'forest_entrance' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const inn = new Room(
|
|
||||||
'inn',
|
|
||||||
'The Rusty Dragon Inn',
|
|
||||||
'A cozy tavern filled with the aroma of hearty stew and ale. Adventurers gather around wooden tables, sharing tales of their exploits.',
|
|
||||||
{ south: 'town_square' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const forestEntrance = new Room(
|
|
||||||
'forest_entrance',
|
|
||||||
'Forest Entrance',
|
|
||||||
'The edge of a dark, mysterious forest. Ancient trees tower overhead, and you can hear strange sounds echoing from within.',
|
|
||||||
{ west: 'town_square', north: 'deep_forest' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const deepForest = new Room(
|
|
||||||
'deep_forest',
|
|
||||||
'Deep Forest',
|
|
||||||
'You are deep within the forest. Shadows dance between the trees, and you feel like you\'re being watched.',
|
|
||||||
{ south: 'forest_entrance' }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.rooms.set('town_square', townSquare);
|
|
||||||
this.rooms.set('inn', inn);
|
|
||||||
this.rooms.set('forest_entrance', forestEntrance);
|
|
||||||
this.rooms.set('deep_forest', deepForest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {WebSocket} ws
|
* @param {WebSocket} ws
|
||||||
*/
|
*/
|
||||||
handleConnection(ws) {
|
onConnectionEstabished(ws) {
|
||||||
console.log('New connection established');
|
console.log('New connection established');
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify(
|
||||||
type: 'message',
|
["m", "Welcome to the WebSocket MUD!\nWhat is your username name?"]
|
||||||
content: 'Welcome to the WebSocket MUD!\nWhat is your character name?'
|
));
|
||||||
}));
|
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
this.onIncomingMessage(parseJson(data));
|
||||||
const message = JSON.parse(data);
|
|
||||||
this.handleMessage(ws, message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing message:', error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
this.handleDisconnection(ws);
|
this.onConnectionClosed(ws);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {WebSocket} ws
|
* @param {WebSocket} ws
|
||||||
* @param {strings} message
|
* @param {strings} message
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
handleMessage(ws, message) {
|
onIncomingMessage(ws, message) {
|
||||||
const player = this.players.get(ws);
|
const player = this.players.get(ws);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
// Player hasn't been created yet, expecting name
|
// Player hasn't been created yet, expecting name
|
||||||
const name = message.content.trim();
|
const name = message.content.trim();
|
||||||
if (name && !this.playersByName.has(name)) {
|
if (name && !this.players.has(name)) {
|
||||||
this.createPlayer(ws, name);
|
this.createPlayer(ws, name);
|
||||||
} else {
|
} else {
|
||||||
|
/**
|
||||||
|
* @todo: send an array instead of object.
|
||||||
|
* element 1 is the type
|
||||||
|
* element 2 is the content
|
||||||
|
* element 3+ are expansions
|
||||||
|
*/
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
content: 'Invalid name or name already taken. Please choose another:'
|
content: 'Invalid name or name already taken. Please choose another:'
|
||||||
@@ -209,9 +88,9 @@ class MudServer {
|
|||||||
createPlayer(ws, name) {
|
createPlayer(ws, name) {
|
||||||
const player = new Player(name, ws);
|
const player = new Player(name, ws);
|
||||||
this.players.set(ws, player);
|
this.players.set(ws, player);
|
||||||
this.playersByName.set(name, player);
|
this.players.set(name, player);
|
||||||
|
|
||||||
const startRoom = this.rooms.get(player.currentRoom);
|
const startRoom = this.rooms.get("town_square");
|
||||||
startRoom.addPlayer(player);
|
startRoom.addPlayer(player);
|
||||||
|
|
||||||
player.sendMessage(`Welcome, ${name}! You have entered the world.`);
|
player.sendMessage(`Welcome, ${name}! You have entered the world.`);
|
||||||
@@ -354,7 +233,7 @@ class MudServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showOnlinePlayers(player) {
|
showOnlinePlayers(player) {
|
||||||
const playerList = Array.from(this.playersByName.keys());
|
const playerList = Array.from(this.players.keys());
|
||||||
player.sendMessage(`Online players (${playerList.length}): ${playerList.join(', ')}`);
|
player.sendMessage(`Online players (${playerList.length}): ${playerList.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +259,10 @@ Available Commands:
|
|||||||
player.sendMessage(helpText);
|
player.sendMessage(helpText);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnection(ws) {
|
/**
|
||||||
|
* Called when a websocket connection is closing.
|
||||||
|
*/
|
||||||
|
onConnectionClosed(ws) {
|
||||||
const player = this.players.get(ws);
|
const player = this.players.get(ws);
|
||||||
if (player) {
|
if (player) {
|
||||||
console.log(`Player ${player.name} disconnected`);
|
console.log(`Player ${player.name} disconnected`);
|
||||||
@@ -393,14 +275,15 @@ Available Commands:
|
|||||||
|
|
||||||
// Clean up references
|
// Clean up references
|
||||||
this.players.delete(ws);
|
this.players.delete(ws);
|
||||||
this.playersByName.delete(player.name);
|
this.players.delete(player.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP server for serving the client
|
// Create HTTP server for serving the client
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);
|
// let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);
|
||||||
|
let filePath = path.join('public', req.url === '/' ? 'index.html' : req.url);
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
|
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
@@ -409,13 +292,15 @@ const server = http.createServer((req, res) => {
|
|||||||
'.html': 'text/html',
|
'.html': 'text/html',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!contentType[ext]) {
|
if (!contentTypes[ext]) {
|
||||||
// Invalid file, pretend it did not exist!
|
// Invalid file, pretend it did not exist!
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end('File not found');
|
res.end('File not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentType = contentTypes[ext];
|
||||||
|
|
||||||
fs.readFile(filePath, (err, data) => {
|
fs.readFile(filePath, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
@@ -428,11 +313,11 @@ const server = http.createServer((req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create WebSocket server
|
// Create WebSocket server
|
||||||
const wss = new WebSocket.Server({ server });
|
const wss = new WebSocketServer({ server });
|
||||||
const mudServer = new MudServer();
|
const mudServer = new MudServer();
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
mudServer.handleConnection(ws);
|
mudServer.onConnectionEstabished(ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|||||||
30
server/utils/password.js
Normal file
30
server/utils/password.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { randomBytes, pbkdf2Sync, randomInt } from 'node:crypto';
|
||||||
|
|
||||||
|
// Settings (tune as needed)
|
||||||
|
const ITERATIONS = 100_000; // Slow enough to deter brute force
|
||||||
|
const KEYLEN = 64; // 512-bit hash
|
||||||
|
const DIGEST = 'sha512';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a hash from a plaintext password.
|
||||||
|
* @param {String} password
|
||||||
|
* @returns String
|
||||||
|
*/
|
||||||
|
export function hash(password) {
|
||||||
|
const salt = randomBytes(16).toString('hex'); // 128-bit salt
|
||||||
|
const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString('hex');
|
||||||
|
return `${ITERATIONS}:${salt}:${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a password is correct against a given hash.
|
||||||
|
*
|
||||||
|
* @param {String} password
|
||||||
|
* @param {String} hashed_password
|
||||||
|
* @returns Boolean
|
||||||
|
*/
|
||||||
|
export function verify(password, hashed_password) {
|
||||||
|
const [iterations, salt, hash] = hashed_password.split(':');
|
||||||
|
const derived = pbkdf2Sync(password, salt, Number(iterations), KEYLEN, DIGEST).toString('hex');
|
||||||
|
return hash === derived;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user