Socket.IO Username Issue

Hi,

I’m currently working on creating a simple chat app, but have run into a problem after trying to display usernames. Everything works great up until the disconnect, where Socket.IO seems to forget the socket’s username and displays “transport close has disconnected” in the Terminal and “undefined has disconnected.” on the app itself.

Does anyone have an idea of how I can fix this, and I’m open to other code improvements as well, I just learned the basics of Socket.IO and designed the username system entirely myself, so I’m not sure if I’m doing it the best way.

(Also, another issue if someone wants to go above and beyond. My code that prevents users from accessing certain usernames (such as Server) doesn’t seem to be working. I’ve already thought of a better way to implement it, but I’m still wondering why it seems to be failing.)


server.js

const path = require("path");
const ejs = require("ejs");

const express = require("express");
const app = express();

const http = require("http");
const server = http.createServer(app);

const { Server } = require("socket.io");
const io = new Server(server);

const PORT = process.env.PORT;

app.use(express.static(path.join(__dirname, "public")));

app.set("view engine", "ejs");

app.get("/", (req, res) => {
    res.render("index");
});

io.on("connection", (socket) => {
    socket.on("username", (username) => {

        socket.broadcast.emit("connected", username);
        console.log(`${username} has connected`);

        socket.on("message", (message) => {
            socket.broadcast.emit("message", message, username);
            console.log(`${username}: ${message}`);
        });
        socket.on("disconnect", (username) => {
            socket.broadcast.emit("disconnected");
            console.log(`${username} has disconnected`);
        });
    });
});

server.listen(PORT, () => {
    console.log(`Listening on *:${PORT}`);
});

client.js

const socket = io();

const form = document.getElementById("form");
const message = document.getElementById("input");

const messages = document.getElementById("messages");

const allowedChars = "abcdefghijklmnopqrstuvwxyz1234567890_";
const invalidUsers = ["Server"];

function getUsername(allowedChars, invalidUsers) {
    let username = null;
    let promptWith = "Enter Username: ";

    function checkChars() {
        for (let i = 0; i <= username.length; i++) {
            if (allowedChars.indexOf(username[i].toLowerCase()) === -1) {
                promptWith =
                    "Username includes invalid characters\nEnter Username: ";
                return false;
            }

            return true;
        }
    }

    function checkUsers() {
        let lowercaseInvalidUsers = [];

        for (i of invalidUsers) {
            lowercaseInvalidUsers.push(i.toLowerCase());
        }

        if (username in lowercaseInvalidUsers) {
            promptWith =
                "Chosen username is reserved or disallowed\nEnter Username: ";
            return false;
        }

        return true;
    }

    while (true) {
        do {
            username = prompt(promptWith);
        } while (username === null || username === "");

        if (!checkChars()) {
            continue;
        } else if (!checkUsers()) {
            continue;
        }

        break;
    }

    return username;
}

const username = getUsername(allowedChars, invalidUsers);
socket.emit("username", username);

function displayMessage(message, username) {
    const msgli = document.createElement("li");

    msgli.innerHTML = `<strong>${username}</strong> <p>${message}</p>`;
    messages.appendChild(msgli);

    window.scrollTo(0, document.body.scrollHeight);
}

form.addEventListener("submit", function (e) {
    e.preventDefault();
    if (message.value) {
        displayMessage(message.value, username);
        socket.emit("message", message.value);
        message.value = "";
    }
});

socket.on("connected", function (username) {
    displayMessage(`${username} has connected.`, "Server");
});

socket.on("disconnected", function (username) {
    displayMessage(`${username} has disconnected.`, "Server");
});

socket.on("message", function (message, username) {
    displayMessage(message, username);
});

If anyone needs my entire project, the GitHub repo can be found below. Thank you!

2 Likes

Two notes here:

  1. The user’s name isn’t guaranteed to be lowercase here.
  2. Why not save the reserved names as lowercase?
1 Like

also you should do username processing in the server side (and maybe the client side also to save network bandwidth)
Along with XSS

2 Likes

Actually yeah, that would save a bunch of time. Thank you!

3 Likes

Yes, or else it’s very easy to bypass. Doxr has had first hand experience with this, I had a bit of fun on his chat app.

2 Likes

I wasn’t really sure how I was supposed to setup actually getting the username from the user, so I just got it from the client side and forwarded it over to the server after I did some checks. Is there a better way to do it?

A bit confused about how XSS is actually executed. If you just processed the user’s message as text and not HTML (like I did with .textContent), wouldn’t it be harmless?

I might not be understanding XSS correctly so sorry if I make no sense

You could keep the username checks in the client.js file, but also add an identical check in the server, which would return an error to the client (which doesn’t have to be pretty, could be silent 400, because this error would only be shown to hackers)

That is true (assuming that .textContent is a safe sink), unless you are planning on doing what element1010 did and allowing HTML through a raw network request.

2 Likes

Adding to this

The in operator (in checkUsers() function) checks whether a property is in an object, not an element in an array. You should use the includes() method instead:

function checkUsers() {
    let lowercaseInvalidUsers = invalidUsers.map(i => i.toLowerCase());
    
    if (lowercaseInvalidUsers.includes(username.toLowerCase())) {
        promptWith = "Chosen username is reserved or disallowed\nEnter Username: ";
        return false;
    }

    return true;
}

Justo to clarify

In JavaScript, the in operator is used to check whether an object has a certain property… So using the in operator with an array checks whether the array has an element at a certain index, not whether the array includes a certain value.

On the other hand, the includes() method is used to check whether an array includes a certain value. So, to check if an array includes a certain value (like username in your case), you should use includes() instead of in.

3 Likes

Sorry, my Python instincts are kicking in :laughing: (I do wish those kinds of keywords were more universal though)

Thank you though, as it seems to be working as intended now! Does anyone have an idea why my username isn’t saving upon disconnect though? I will try making username checks server side, and see if that helps.

3 Likes

Use cookies, because each socket.IO connection is a different and totally unique user with no device IDs.

I look through the docs and found out that Socket.io does not keep track of the username automatically, so you should store it manually on the socket object itself.

io.on("connection", (socket) => {
    socket.on("username", (username) => {
        socket.username = username; // Store the username on the socket

        socket.broadcast.emit("connected", username);
        console.log(`${username} has connected`);

        socket.on("message", (message) => {
            socket.broadcast.emit("message", message, username);
            console.log(`${username}: ${message}`);
        });
    });

    socket.on("disconnect", () => {
        socket.broadcast.emit("disconnected", socket.username); //you can change this to not use the store username too
        console.log(`${socket.username} has disconnected`); // same thing here
    });
});

1 Like

There is this too:

"Please note that, unless connection state recovery is enabled, the id attribute is an ephemeral ID that is not meant to be used in your application (or only for debugging purposes) because:

  • this ID is regenerated after each reconnection (for example when the WebSocket connection is severed, or when the user refreshes the page)
  • two different browser tabs will have two different IDs
  • there is no message queue stored for a given ID on the server (i.e. if the client is disconnected, the messages sent from the server to this ID are lost)

Please use a regular session ID instead (either sent in a cookie, or stored in the localStorage and sent in the auth payload)."

3 Likes

or in the body as an auth param. If you want to identify your clients, you might as well reinvent replit auth.

EDIT: I just got TL3!!! Also, how to you make the little “3” icon appear in your profile?

I’m so confused, for the past 20 minutes my program has been crashing non-stop and I can’t figure out why. It’s not like there is an error in my code kind of crash, node itself is crashing. I’m developing using GitHub Codespaces, and it just randomly started happening.

I get this error from start to finish:

[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`
Listening on *:3000

<--- Last few GCs --->
 =[34647:0x737a950]    49155 ms: Mark-Compact (reduce) 2048.0 (2082.1) -> 2047.2 (2082.4) MB, 1463.72 / 0.00 ms  (+ 57.7 ms in 15 steps since start of marking, biggest step 9.1 ms, walltime since start of marking 1533 ms) (average mu = 0.283, current mu = 0[34647:0x737a950]    51187 ms: Mark-Compact (reduce) 2048.2 (2082.4) -> 2047.5 (2082.6) MB, 1502.83 / 0.00 ms  (+ 51.9 ms in 15 steps since start of marking, biggest step 3.9 ms, walltime since start of marking 1562 ms) (average mu = 0.259, current mu = 0

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
 1: 0xc8e4a0 node::Abort() [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 2: 0xb6b8f3  [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 3: 0xeacb10 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 4: 0xeacdf7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 5: 0x10be465  [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 6: 0x10d62e8 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 7: 0x10ac401 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 8: 0x10ad595 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
 9: 0x108ab06 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
10: 0x14e5936 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/usr/local/share/nvm/versions/node/v20.3.0/bin/node]
11: 0x7fe813699ef6

Are you running on a low-end machine?

1 Like

Could be, I don’t think my application would be that resource intensive though. I have 4 cores and 8 GB of RAM on my Codespace.

Do you want me to create a Live Share if that helps you troubleshoot?

Edit: Ok, I will message you a link to join soon
Edit 2: Just sent you a message with the join link

on codespaces or vscode? okey, I’ve used live share before so that would be nice.

I think your issue may just be that this:

Needs to be inside this:

socket.on("connect", function() {});

Otherwise it may try to emit before the connection is actually established, although I could be completely wrong and it may just add to a queue to be sent once a connection is established or something, but I don’t think that is the case.

1 Like

I’m currently working on bug fixes w/ bobastley, it was actually bc of a while True loop which constantly adds event listeners to the socket, causing the memory to overflow after 30 seconds or so.

1 Like