From 07685208392fcfe8f0ddf55cef47c4c867c0290c Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 09:07:57 +0200 Subject: [PATCH 01/19] Fix password comand behavior to match real protocol --- src/server.js | 16 +++++++++++++--- src/user.js | 6 +++++- test/command_pass.js | 18 ++++++++++++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/server.js b/src/server.js index f6da4e4..7c7138b 100644 --- a/src/server.js +++ b/src/server.js @@ -13,10 +13,17 @@ const ERR_NEEDMOREPARAMS = '461' let server = {} -server.create = function create() { +server.create = function create(config = {}) { + let password + if (config.password) { + password = config.password + } let userlist = {} let server = net.createServer((socket) => { const user = new User(socket) + if (!config.password) { + user.authenticated = true + } socket.on('data', function (data) { data.toString("ascii").split("\r\n").forEach(function (commandline) { if (commandline == "") { @@ -76,8 +83,11 @@ server.create = function create() { server.closeConnection(user.nickname) break; case "PASS": - if (tokenized[1]) - user.setPassword(tokenized[1]) + if (tokenized[1] === config.password) { + user.authenticated = true + } else { + user.closeConnection() + } break; default: diff --git a/src/user.js b/src/user.js index f207aef..b54678e 100644 --- a/src/user.js +++ b/src/user.js @@ -1,5 +1,6 @@ function User(socket) { this.registered = false + this.authenticated = false this.nickname = "" this.connection = socket this.realname = "" @@ -17,6 +18,9 @@ function User(socket) { } this.register = function(username, realname) { + if (!this.authenticated) { + this.connection.destroy() + } this.username = username this.realname = realname this.registered = true @@ -33,7 +37,7 @@ function User(socket) { } this.closeConnection = function () { - socket.destroy() + this.connection.destroy() } diff --git a/test/command_pass.js b/test/command_pass.js index 36e605d..653a25a 100644 --- a/test/command_pass.js +++ b/test/command_pass.js @@ -4,21 +4,31 @@ const IRCServer = require("../src/server.js"); describe("PASS OK", function () { it("should handle a PASS command -> PASS some_passwd", function (done) { - const server = IRCServer.create() + const server = IRCServer.create({ + password: "itsJustATest" + }) let mockedSock = new EventEmitter() + mockedSock.address = function() { + return { port: 12346, family: 'IPv4', address: '127.0.0.1' } + } + + mockedSock.write = function (data) { + return + } mockedSock.destroy = function () { done("Destroyed socket without answering") } server.emit("connection", mockedSock) - mockedSock.emit('data', Buffer.from("PASS some_pass\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("PASS itsJustATest\r\n", "ascii")) mockedSock.emit('data', Buffer.from("NICK some_nick\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) user = server.getUserlist()["some_nick"] - assert.equal("some_pass", user["password"]) + assert.equal(user["registered"], true) done() }) -}) \ No newline at end of file +}) From 6ffedcd74e47cd0c71119200cb5078a41b2e2ef5 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 09:10:45 +0200 Subject: [PATCH 02/19] Add test for wrong password --- test/command_pass.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/command_pass.js b/test/command_pass.js index 653a25a..0bbdb57 100644 --- a/test/command_pass.js +++ b/test/command_pass.js @@ -29,6 +29,29 @@ describe("PASS OK", function () { user = server.getUserlist()["some_nick"] assert.equal(user["registered"], true) done() + }) + + it("should end connection on wrong password", function (done) { + const server = IRCServer.create({ + password: "itsJustATest" + }) + let mockedSock = new EventEmitter() + + mockedSock.address = function() { + return { port: 12346, family: 'IPv4', address: '127.0.0.1' } + } + + mockedSock.write = function (data) { + return + } + + mockedSock.destroy = function () { + // connection has to be destroyed + done("") + } + + server.emit("connection", mockedSock) + mockedSock.emit('data', Buffer.from("PASS wrongpassword\r\n", "ascii")) }) }) From f816cd1adad6c29df25c97b754bed59c30862206 Mon Sep 17 00:00:00 2001 From: wayne Date: Wed, 17 Jul 2019 12:23:56 +0200 Subject: [PATCH 03/19] added test case to not send to unregistered users --- src/server.js | 140 +++++++++++++++++++++------------------- test/command_privmsg.js | 57 +++++++++++++--- 2 files changed, 121 insertions(+), 76 deletions(-) diff --git a/src/server.js b/src/server.js index 7c7138b..988abd9 100644 --- a/src/server.js +++ b/src/server.js @@ -11,89 +11,95 @@ const ERR_NONICKNAMEGIVEN = '431' const ERR_NICKNAMEINUSE = '433' const ERR_NEEDMOREPARAMS = '461' + let server = {} server.create = function create(config = {}) { let password if (config.password) { - password = config.password + password = config.password } let userlist = {} let server = net.createServer((socket) => { const user = new User(socket) if (!config.password) { - user.authenticated = true + user.authenticated = true } socket.on('data', function (data) { data.toString("ascii").split("\r\n").forEach(function (commandline) { - if (commandline == "") { - return - } - let splitted = commandline.split(" ") - let tokenized = [] - let lastParam = -1 - for (let i = 0; i < splitted.length; i++) { - if (lastParam > 0) { - tokenized[lastParam] = tokenized[lastParam] + " " + splitted[i] - } else if (splitted[i].charAt(0) == ":" && i > 0) { - lastParam = i - tokenized[lastParam] = splitted[i].slice(1) - } else { - tokenized[i] = splitted[i] - } - } - let command = tokenized[0].toUpperCase() - switch (command) { - case "PING": - if (tokenized[1] && tokenized[1] === "irc.example.com") { - socket.write("PONG irc.example.com\r\n") - } - break; - case "NICK": - if (tokenized[1]) { - let nickname = tokenized[1] - // nick collision test - if (!Object.keys(userlist).includes(nickname)) { - user.setNickname(nickname) - userlist[user.nickname] = user - } else { - socket.write(ERR_NICKNAMEINUSE, " nickname in use") - } - - - } else { - socket.write(ERR_NONICKNAMEGIVEN, "ERROR: NO NICKNAME PROVIDED") - } - - break; - - case "USER": - // ignoring servername and hostname, not useful these days - user.register(tokenized[1], tokenized[4]) - let address = user.getAddress() - socket.write(`:irc.example.com 001 ${user.nickname} :Welcome to the example IRC Project ${user.nickname}!~${user.username}@${address}\r\n`, "ascii") - break; - case "PRIVMSG": - let target = userlist[tokenized[1]] - let message = tokenized[2] - - target.sendMsg(user, message) - break; - case "QUIT": - server.closeConnection(user.nickname) - break; - case "PASS": - if (tokenized[1] === config.password) { - user.authenticated = true + if (commandline == "") { + return + } + let splitted = commandline.split(" ") + let tokenized = [] + let lastParam = -1 + for (let i = 0; i < splitted.length; i++) { + if (lastParam > 0) { + tokenized[lastParam] = tokenized[lastParam] + " " + splitted[i] + } else if (splitted[i].charAt(0) == ":" && i > 0) { + lastParam = i + tokenized[lastParam] = splitted[i].slice(1) } else { - user.closeConnection() + tokenized[i] = splitted[i] } - break; + } + let command = tokenized[0].toUpperCase() + switch (command) { + case "PING": + if (tokenized[1] && tokenized[1] === "irc.example.com") { + socket.write("PONG irc.example.com\r\n") + } + break; + case "NICK": + if (tokenized[1]) { + let nickname = tokenized[1] + // nick collision test + if (!Object.keys(userlist).includes(nickname)) { + user.setNickname(nickname) + userlist[user.nickname] = user + } else { + socket.write(ERR_NICKNAMEINUSE, " nickname in use") + } + + + } else { + socket.write(ERR_NONICKNAMEGIVEN, "ERROR: NO NICKNAME PROVIDED") + } + + break; + + case "USER": + // ignoring servername and hostname, not useful these days + user.register(tokenized[1], tokenized[4]) + let address = user.getAddress() + socket.write(`:irc.example.com 001 ${user.nickname} :Welcome to the example IRC Project ${user.nickname}!~${user.username}@${address}\r\n`, "ascii") + break; + case "PRIVMSG": + let target = userlist[tokenized[1]] + let message = tokenized[2] + if (target.registered === false) { + socket.write(`:irc.example.com ${ERR_NOSUCHNICK} ${target} no such nick/channel`) + + } else { + target.sendMsg(user, message) + + } + break; + case "QUIT": + server.closeConnection(user.nickname) + break; + case "PASS": + if (tokenized[1] === config.password) { + user.authenticated = true + } else { + user.closeConnection() + } + break; - default: - console.error(`Unknown command: ${command}`); - } - }); + default: + console.error(`Unknown command: ${command}`); + } + }); }).on('error', (err) => { console.error(err); }) diff --git a/test/command_privmsg.js b/test/command_privmsg.js index d464e0b..10df41e 100644 --- a/test/command_privmsg.js +++ b/test/command_privmsg.js @@ -29,8 +29,8 @@ describe("PRIVMSG OK", function () { it("should handle a PRIVMSG command for multiple users -> PRIVMSG other_nick :I'm a message", function (done) { const server = IRCServer.create() let mockedSock1 = new EventEmitter() - mockedSock1.address = function() { - return { port: 12346, family: 'IPv4', address: '127.0.0.1' } + mockedSock1.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} } mockedSock1.write = function (data) { return @@ -39,15 +39,15 @@ describe("PRIVMSG OK", function () { done("Destroyed socket without answering") } let mockedSock2 = new EventEmitter() - mockedSock2.address = function() { - return { port: 12346, family: 'IPv4', address: '127.0.0.1' } + mockedSock2.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} } mockedSock2.write = function (data) { - let answer = data.toString("ascii") - if (answer.indexOf("PRIVMSG") >= 0) { - assert.equal(answer, ":some_nick PRIVMSG other_nick :I'm a message\r\n") - done() - } + let answer = data.toString("ascii") + if (answer.indexOf("PRIVMSG") >= 0) { + assert.equal(answer, ":some_nick PRIVMSG other_nick :I'm a message\r\n") + done() + } } mockedSock2.destroy = function () { done("Destroyed socket without answering") @@ -60,6 +60,45 @@ describe("PRIVMSG OK", function () { mockedSock2.emit('data', Buffer.from("NICK other_nick\r\n", "ascii")) mockedSock2.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock1.emit('data', Buffer.from("PRIVMSG other_nick :I'm a message\r\n", "ascii")) + }) + it("dont send message to unregistered user", function (done) { + const server = IRCServer.create() + let mockedSock1 = new EventEmitter() + mockedSock1.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock1.write = function (data) { + answer = data.toString('ascii') + if (answer.indexOf("401")>=0) { + assert.equal(answer, ":irc.example.com 401 some_nick no such nick/channel") + } + done() + } + mockedSock1.destroy = function () { + done("Destroyed socket without answering") + } + let mockedSock2 = new EventEmitter() + mockedSock2.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock2.write = function (data) { + let answer = data.toString("ascii") + if (answer.indexOf("PRIVMSG") >= 0) { + assert.equal(answer, ":some_nick PRIVMSG other_nick :I'm a message\r\n") + done("I got a message that I shouldn't have seen") + } + } + mockedSock2.destroy = function () { + done("Destroyed socket without answering") + } + + server.emit("connection", mockedSock1) + server.emit("connection", mockedSock2) + mockedSock1.emit('data', Buffer.from("NICK some_nick\r\n", "ascii")) + mockedSock1.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock2.emit('data', Buffer.from("NICK other_nick\r\n", "ascii")) + mockedSock1.emit('data', Buffer.from("PRIVMSG other_nick :I'm a message\r\n", "ascii")) }) }) From d61f97d0348e08a5123ae8826c5d5a530f17a4a2 Mon Sep 17 00:00:00 2001 From: wayne Date: Wed, 17 Jul 2019 12:28:24 +0200 Subject: [PATCH 04/19] added: do not expect a error message for successful QUIT commands --- test/command_quit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/command_quit.js b/test/command_quit.js index f4828a8..4ccd8ef 100644 --- a/test/command_quit.js +++ b/test/command_quit.js @@ -11,6 +11,7 @@ describe("QUIT OK", function () { let mockedSock = new EventEmitter() mockedSock.write = function (data) { + done("ERROR: did not expect a message") } server.emit('connection', mockedSock) From 233a9aef72f969d6c146164329b957d6f6ff2f67 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 12:14:05 +0200 Subject: [PATCH 05/19] Add concept of channels and allow users to create them --- src/channel.js | 11 +++++++++++ src/server.js | 8 ++++++++ src/user.js | 3 +++ test/command_join.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 src/channel.js create mode 100644 test/command_join.js diff --git a/src/channel.js b/src/channel.js new file mode 100644 index 0000000..4eab6ae --- /dev/null +++ b/src/channel.js @@ -0,0 +1,11 @@ +function Channel(name) { + this.name = name + this.userlist = [] + + this.join = function(user) { + this.userlist.push(user) + user.sendRaw(`:${user.nickname} JOIN ${this.name}`) + } +} + +module.exports = Channel diff --git a/src/server.js b/src/server.js index 988abd9..46d25fb 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,6 @@ const net = require("net") const User = require("../src/user.js") +const Channel = require("../src/channel.js") const RPL_WELCOME = '001' const ERR_NOSUCHNICK = '401' @@ -20,6 +21,7 @@ server.create = function create(config = {}) { password = config.password } let userlist = {} + let channellist = {} let server = net.createServer((socket) => { const user = new User(socket) if (!config.password) { @@ -85,6 +87,12 @@ server.create = function create(config = {}) { } break; + case "JOIN": + let channelname = tokenized[1] + let channel = new Channel(channelname) + channel.join(user) + channellist[channelname] = channel + break; case "QUIT": server.closeConnection(user.nickname) break; diff --git a/src/user.js b/src/user.js index b54678e..a9b454f 100644 --- a/src/user.js +++ b/src/user.js @@ -44,6 +44,9 @@ function User(socket) { this.sendMsg = function (from, message) { this.connection.write(`:${from.nickname} PRIVMSG ${this.nickname} :${message}\r\n`, "ascii") } + this.sendRaw = function(message) { + this.connection.write(`${message}\r\n`, "ascii") + } } module.exports = User diff --git a/test/command_join.js b/test/command_join.js new file mode 100644 index 0000000..da2a65a --- /dev/null +++ b/test/command_join.js @@ -0,0 +1,28 @@ +const assert = require('assert'); +const EventEmitter = require('events'); +const IRCServer = require("../src/server.js"); + +describe("JOIN OK", function () { + it("should handle a JOIN command -> JOIN #testchan", function (done) { + const server = IRCServer.create() + let mockedSock = new EventEmitter() + mockedSock.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock.write = function (data) { + let answer = data.toString("ascii") + if (answer.indexOf("JOIN") >= 0) { + assert.equal(answer, ":some_nick JOIN #testchan\r\n") + done() + } + } + mockedSock.destroy = function () { + done("Destroyed socket without answering") + } + + server.emit("connection", mockedSock) + mockedSock.emit('data', Buffer.from("NICK some_nick\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("JOIN #testchan\r\n", "ascii")) + }) +}) From 394384689b7d8467c5b4a96cd187499bebb33d3e Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 12:39:19 +0200 Subject: [PATCH 06/19] Add ability to join channel with multiple users Joining channels allone is nice, but defeats the purpose of channels. This patch adds the ability to join a channel with multiple users. --- src/channel.js | 5 ++++- src/server.js | 11 ++++++++--- test/command_join.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/channel.js b/src/channel.js index 4eab6ae..f4d82f3 100644 --- a/src/channel.js +++ b/src/channel.js @@ -3,8 +3,11 @@ function Channel(name) { this.userlist = [] this.join = function(user) { + let channel = this this.userlist.push(user) - user.sendRaw(`:${user.nickname} JOIN ${this.name}`) + this.userlist.forEach(function(item) { + item.sendRaw(`:${user.nickname} JOIN ${channel.name}`) + }) } } diff --git a/src/server.js b/src/server.js index 46d25fb..63e8fc3 100644 --- a/src/server.js +++ b/src/server.js @@ -89,9 +89,14 @@ server.create = function create(config = {}) { break; case "JOIN": let channelname = tokenized[1] - let channel = new Channel(channelname) - channel.join(user) - channellist[channelname] = channel + if (Object.keys(channellist).includes(channelname)) { + channellist[channelname].join(user) + } else { + let channel = new Channel(channelname) + channel.join(user) + channellist[channelname] = channel + } + break; case "QUIT": server.closeConnection(user.nickname) diff --git a/test/command_join.js b/test/command_join.js index da2a65a..6ea8ac3 100644 --- a/test/command_join.js +++ b/test/command_join.js @@ -25,4 +25,48 @@ describe("JOIN OK", function () { mockedSock.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) mockedSock.emit('data', Buffer.from("JOIN #testchan\r\n", "ascii")) }) + + it("should handle a JOIN command for more than one user -> JOIN #testchan and send join events to all channel members", function (done) { + const server = IRCServer.create() + // helper to count join messages for channel + // we should see 2 of them + let counter = 0 + let mockedSock1 = new EventEmitter() + mockedSock1.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock1.write = function (data) { + let answer = data.toString("ascii") + if (answer.indexOf("JOIN") >= 0 && counter === 1) { + assert.equal(answer, ":other_nick JOIN #testchan\r\n") + done() + } + if (answer === ":some_nick JOIN #testchan\r\n") { + counter++ + } + } + mockedSock1.destroy = function () { + done("Destroyed socket without answering") + } + + let mockedSock2 = new EventEmitter() + mockedSock2.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock2.write = function (data) { + return + } + mockedSock2.destroy = function () { + done("Destroyed socket without answering") + } + + server.emit("connection", mockedSock1) + server.emit("connection", mockedSock2) + mockedSock1.emit('data', Buffer.from("NICK some_nick\r\n", "ascii")) + mockedSock1.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock1.emit('data', Buffer.from("JOIN #testchan\r\n", "ascii")) + mockedSock2.emit('data', Buffer.from("NICK other_nick\r\n", "ascii")) + mockedSock2.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock2.emit('data', Buffer.from("JOIN #testchan\r\n", "ascii")) + }) }) From a9c6b0d1dc13ea24d58c938de710c52423265dbc Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 13:02:10 +0200 Subject: [PATCH 07/19] Rework user.sendMsg() to use user.sendRaw() --- src/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user.js b/src/user.js index a9b454f..f59004d 100644 --- a/src/user.js +++ b/src/user.js @@ -42,7 +42,7 @@ function User(socket) { this.sendMsg = function (from, message) { - this.connection.write(`:${from.nickname} PRIVMSG ${this.nickname} :${message}\r\n`, "ascii") + this.sendRaw(`:${from.nickname} PRIVMSG ${this.nickname} :${message}`, "ascii") } this.sendRaw = function(message) { this.connection.write(`${message}\r\n`, "ascii") From 4ac6e94b60e60cd1b698285e71fa0902091d6692 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 21:10:06 +0200 Subject: [PATCH 08/19] Remove unused setPassword function --- src/user.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/user.js b/src/user.js index f59004d..d2400c6 100644 --- a/src/user.js +++ b/src/user.js @@ -13,9 +13,6 @@ function User(socket) { this.getNickname = function(nickname) { return this.nickname } - this.setPassword = function (password) { - this.password = password - } this.register = function(username, realname) { if (!this.authenticated) { From b7bc3f3d4c0509d005c07303bd60041e37581ec3 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 21:20:05 +0200 Subject: [PATCH 09/19] Remove external grab into the object This change removes server object interacting with an internal variable from the user object creation and reduces the amount of code from 3 to 1 line. This also simplifies future testing. --- src/server.js | 5 +---- src/user.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/server.js b/src/server.js index 63e8fc3..0d12a46 100644 --- a/src/server.js +++ b/src/server.js @@ -23,10 +23,7 @@ server.create = function create(config = {}) { let userlist = {} let channellist = {} let server = net.createServer((socket) => { - const user = new User(socket) - if (!config.password) { - user.authenticated = true - } + const user = new User(socket, !config.password) socket.on('data', function (data) { data.toString("ascii").split("\r\n").forEach(function (commandline) { if (commandline == "") { diff --git a/src/user.js b/src/user.js index d2400c6..85030e0 100644 --- a/src/user.js +++ b/src/user.js @@ -1,6 +1,6 @@ -function User(socket) { +function User(socket, authenticatedDefault) { this.registered = false - this.authenticated = false + this.authenticated = authenticatedDefault this.nickname = "" this.connection = socket this.realname = "" From b55e1cc810d08a7294642615b4b9a6be4152f47b Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 21:28:15 +0200 Subject: [PATCH 10/19] Add basic unit test file for users After learning today that testing the leaves is more important, some fixing for the unit test coverage towards users. Time to do better. --- test/user.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/user.js diff --git a/test/user.js b/test/user.js new file mode 100644 index 0000000..35dc96b --- /dev/null +++ b/test/user.js @@ -0,0 +1,54 @@ +const assert = require('assert'); +const User = require("../src/user.js"); + +describe('User', function () { + describe('#contructor()', function () { + it("should create a user while no password is required", function () { + let mockedSock = {} + const user = new User(mockedSock, true) + assert.equal(typeof user, 'object') + }) + + it("should create a user while password is required", function () { + let mockedSock = {} + const user = new User(mockedSock, false) + assert.equal(typeof user, 'object') + }) + }) + + describe('#sendMsg(from, message)', function() { + it("should send a message to the user's socket", function (done) { + let mockedSock = {write: function (data) { + assert.equal(data, ":some_nick PRIVMSG some_nick :test message\r\n") + done() + }} + const user = new User(mockedSock, true) + user.setNickname("some_nick") + user.register("some_nick") + user.sendMsg(user, "test message") + }) + }) + + describe('#sendRaw(message)', function() { + it("should send a raw command to the user's socket", function(done) { + let mockedSock = {write: function (data) { + assert.equal(data, ":irc.example.com TEST this :command with parameters\r\n") + done() + }} + const user = new User(mockedSock, true) + user.setNickname("some_nick") + user.register("some_nick") + user.sendRaw(":irc.example.com TEST this :command with parameters") + }) + }) + + describe('#closeConnection()', function() { + it("should call destroy on user's socket", function(done) { + let mockedSock = {destroy: function () { + done() + }} + const user = new User(mockedSock, true) + user.closeConnection() + }) + }) +}) From ba6a424bfe2e20fa2200ed94087e28e85aa0b955 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 21:36:20 +0200 Subject: [PATCH 11/19] Implement non-direct messages to users In order to send channel messages, we need to set a different target than the user himself. This patch adds a new parameter `to` to the `sendMsg()` function of the user object and allows to provide a string as target. This uses by default the user's nickname as target, which makes the parameter optional and doesn't break the existing interface. --- src/user.js | 4 ++-- test/user.js | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/user.js b/src/user.js index 85030e0..5df55bb 100644 --- a/src/user.js +++ b/src/user.js @@ -38,8 +38,8 @@ function User(socket, authenticatedDefault) { } - this.sendMsg = function (from, message) { - this.sendRaw(`:${from.nickname} PRIVMSG ${this.nickname} :${message}`, "ascii") + this.sendMsg = function (from, message, to = this.nickname) { + this.sendRaw(`:${from.nickname} PRIVMSG ${to} :${message}`, "ascii") } this.sendRaw = function(message) { this.connection.write(`${message}\r\n`, "ascii") diff --git a/test/user.js b/test/user.js index 35dc96b..19008ae 100644 --- a/test/user.js +++ b/test/user.js @@ -16,8 +16,8 @@ describe('User', function () { }) }) - describe('#sendMsg(from, message)', function() { - it("should send a message to the user's socket", function (done) { + describe('#sendMsg(from, message, to)', function() { + it("should send a message to the user's socket with the user himself as target", function (done) { let mockedSock = {write: function (data) { assert.equal(data, ":some_nick PRIVMSG some_nick :test message\r\n") done() @@ -27,6 +27,17 @@ describe('User', function () { user.register("some_nick") user.sendMsg(user, "test message") }) + + it("should send a message to the user's socket with a different target", function (done) { + let mockedSock = {write: function (data) { + assert.equal(data, ":some_nick PRIVMSG #testchan :test message\r\n") + done() + }} + const user = new User(mockedSock, true) + user.setNickname("some_nick") + user.register("some_nick") + user.sendMsg(user, "test message", "#testchan") + }) }) describe('#sendRaw(message)', function() { From e879f3814e5d6d8c60104a5247e4eeadef42e37a Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 20:45:34 +0200 Subject: [PATCH 12/19] Add real unit tests for channel Before we tested channel implicitly by running it through the tests on the server.js. After some learning today, it's time to add an own set of unit tests to channel. --- test/channel.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/channel.js diff --git a/test/channel.js b/test/channel.js new file mode 100644 index 0000000..2c32c5e --- /dev/null +++ b/test/channel.js @@ -0,0 +1,23 @@ +const assert = require('assert'); +const Channel = require("../src/channel.js"); + +describe('Channel', function () { + describe('#contructor()', function () { + it("should create a channel", function () { + const channel = new Channel("#testchan") + assert.equal(typeof channel, 'object') + }) + }) + + describe('#join(user)', function () { + it("should add the user to a channel and send a JOIN command to the user", function (done) { + const channel = new Channel("#testchan") + let mockedUser = {nickname: "some_nick"} + mockedUser.sendRaw = function(data) { + assert.equal(data, ":some_nick JOIN #testchan") + done() + } + channel.join(mockedUser) + }) + }) +}) From fd860b54eea4adade9eb1d46b66c507134151d34 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 20:59:32 +0200 Subject: [PATCH 13/19] Implement channel parting on the channel object This patch allows the channel to part users from it. This provides the basics for the future implementation of the PART command. --- src/channel.js | 11 +++++++++++ test/channel.js | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/channel.js b/src/channel.js index f4d82f3..cf0eddf 100644 --- a/src/channel.js +++ b/src/channel.js @@ -9,6 +9,17 @@ function Channel(name) { item.sendRaw(`:${user.nickname} JOIN ${channel.name}`) }) } + + this.part = function(user) { + let channel = this + let userIndex = this.userlist.indexOf(user) + if (userIndex >= 0) { + this.userlist.forEach(function(item) { + item.sendRaw(`:${user.nickname} PART ${channel.name}`) + }) + this.userlist.splice(index, 1) + } + } } module.exports = Channel diff --git a/test/channel.js b/test/channel.js index 2c32c5e..68accce 100644 --- a/test/channel.js +++ b/test/channel.js @@ -20,4 +20,22 @@ describe('Channel', function () { channel.join(mockedUser) }) }) + + describe('#part(user)', function () { + it("should remove a user from a channel and send a PART command to the user", function (done) { + const channel = new Channel("#testchan") + let mockedUser = {nickname: "some_nick"} + mockedUser.sendRaw = function(data) { + // we expect a PART message from our user to the channel (including us) + if (data.indexOf("PART") >= 0) { + assert.equal(data, ":some_nick PART #testchan") + done() + } + } + // we can't part a channel without joining it first + channel.join(mockedUser) + + channel.part(mockedUser) + }) + }) }) From 0251dda8add24c5c43b100d9a9fcdd324c4a403c Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 21:45:29 +0200 Subject: [PATCH 14/19] Add sendMsg function to channel class This function implements sending messages to all users in a channel while sending the channel name as target to the user's socket. --- src/channel.js | 7 +++++++ test/channel.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/channel.js b/src/channel.js index cf0eddf..b6cb5ee 100644 --- a/src/channel.js +++ b/src/channel.js @@ -20,6 +20,13 @@ function Channel(name) { this.userlist.splice(index, 1) } } + + this.sendMsg = function(from, message) { + const channel = this + this.userlist.forEach(function(item) { + item.sendMsg(from, message, channel.name) + }) + } } module.exports = Channel diff --git a/test/channel.js b/test/channel.js index 68accce..870a640 100644 --- a/test/channel.js +++ b/test/channel.js @@ -38,4 +38,21 @@ describe('Channel', function () { channel.part(mockedUser) }) }) + + describe('#sendMsg(from, message)', function () { + it("should remove a user from a channel and send a PART command to the user", function (done) { + const channel = new Channel("#testchan") + let mockedUser = {nickname: "some_nick", sendRaw: function() {}} + mockedUser.sendMsg = function(from, message, to) { + assert.equal(from, mockedUser) + assert.equal(message, "test message") + assert.equal(to, "#testchan") + done() + } + // we can't part a channel without joining it first + channel.join(mockedUser) + + channel.sendMsg(mockedUser, "test message") + }) + }) }) From 46efd787345dbbb814eaef6d11ba60e23ff42f99 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 22:01:36 +0200 Subject: [PATCH 15/19] Fix nickname error responses to match RFC --- src/server.js | 4 ++-- test/command_nick.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server.js b/src/server.js index 0d12a46..7c89dbd 100644 --- a/src/server.js +++ b/src/server.js @@ -57,12 +57,12 @@ server.create = function create(config = {}) { user.setNickname(nickname) userlist[user.nickname] = user } else { - socket.write(ERR_NICKNAMEINUSE, " nickname in use") + socket.write(`:irc.example.com ${ERR_NICKNAMEINUSE} ${nickname} :Nickname is already in use`, 'ascii') } } else { - socket.write(ERR_NONICKNAMEGIVEN, "ERROR: NO NICKNAME PROVIDED") + socket.write(`:irc.example.com ${ERR_NONICKNAMEGIVEN} :No nickname given`, 'ascii') } break; diff --git a/test/command_nick.js b/test/command_nick.js index 19b0fd1..7c7e720 100644 --- a/test/command_nick.js +++ b/test/command_nick.js @@ -31,7 +31,7 @@ describe("NICK already registered", function () { } let mockedSock2 = new EventEmitter() mockedSock2.write = function (data) { - assert.equal(data.toString("ascii"), "433") + assert.equal(data.toString("ascii"), ":irc.example.com 433 some_nick :Nickname is already in use") done() } mockedSock2.destroy = function () { @@ -49,7 +49,7 @@ describe("NICK no NICK given", function () { const server = IRCServer.create() let mockedSock = new EventEmitter() mockedSock.write = function (data) { - assert.equal(data.toString("ascii"), "431") + assert.equal(data.toString("ascii"), ":irc.example.com 431 :No nickname given") done() } mockedSock.destroy = function () { From 353ca60a003f7545adc2e957e944304726d94a47 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 22:23:16 +0200 Subject: [PATCH 16/19] Fix NICK command behaviour to RFC according to the RFC the correct response to the NICK command then it's changing a nickname is to send a NICK command to the user from the original nickname. This patch changes the behaviour and removes a duplicate declearation of the function. --- src/user.js | 11 ++++------- test/user.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/user.js b/src/user.js index 5df55bb..804f5bf 100644 --- a/src/user.js +++ b/src/user.js @@ -1,12 +1,15 @@ function User(socket, authenticatedDefault) { this.registered = false this.authenticated = authenticatedDefault - this.nickname = "" + this.nickname = undefined this.connection = socket this.realname = "" this.username = "" this.setNickname = function(nickname) { + if (this.nickname !== undefined) { + this.sendRaw(`:${this.nickname} NICK ${nickname}`) + } this.nickname = nickname } @@ -26,12 +29,6 @@ function User(socket, authenticatedDefault) { this.getAddress = function() { return this.connection.address().address } - this.setNickname = function (nickname) { - this.nickname = nickname - } - this.setRealName = function (realname) { - this.realname = realname - } this.closeConnection = function () { this.connection.destroy() diff --git a/test/user.js b/test/user.js index 19008ae..b0d075f 100644 --- a/test/user.js +++ b/test/user.js @@ -62,4 +62,23 @@ describe('User', function () { user.closeConnection() }) }) + + describe('#setNickname(nickname)', function() { + it('should not answer on inital setting of nickname', function () { + let mockedSock = {} + const user = new User(mockedSock, true) + user.setNickname("some_nick") + assert.equal(user.getNickname(), "some_nick") + }) + + it('should answer with NICK message from original nick on rename', function (done) { + let mockedSock = {write: function (data) { + assert.equal(data, ":some_nick NICK changed_nick\r\n") + done() + }} + const user = new User(mockedSock, true) + user.setNickname("some_nick") + user.setNickname("changed_nick") + }) + }) }) From b0d5e41bd9f3675ef8442c0d0e99f92639110d74 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 22:37:14 +0200 Subject: [PATCH 17/19] Add ability to send messages to channel --- src/server.js | 17 ++++++++++++----- test/command_privmsg.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/server.js b/src/server.js index 0d12a46..6ee62f9 100644 --- a/src/server.js +++ b/src/server.js @@ -74,15 +74,22 @@ server.create = function create(config = {}) { socket.write(`:irc.example.com 001 ${user.nickname} :Welcome to the example IRC Project ${user.nickname}!~${user.username}@${address}\r\n`, "ascii") break; case "PRIVMSG": - let target = userlist[tokenized[1]] + let target let message = tokenized[2] - if (target.registered === false) { - socket.write(`:irc.example.com ${ERR_NOSUCHNICK} ${target} no such nick/channel`) - + if (typeof tokenized[1] === 'string' && tokenized[1].charAt(0) === "#") { + target = channellist[tokenized[1]] } else { + let targetUser = userlist[tokenized[1]] + if (targetUser && targetUser.registered) { + target = targetUser + } + } + if (target) { target.sendMsg(user, message) - + } else { + user.sendRaw(`:irc.example.com ${ERR_NOSUCHNICK} ${target} no such nick/channel`) } + break; case "JOIN": let channelname = tokenized[1] diff --git a/test/command_privmsg.js b/test/command_privmsg.js index 10df41e..d1d433f 100644 --- a/test/command_privmsg.js +++ b/test/command_privmsg.js @@ -101,4 +101,28 @@ describe("PRIVMSG OK", function () { mockedSock1.emit('data', Buffer.from("PRIVMSG other_nick :I'm a message\r\n", "ascii")) }) + + it("should allow to send message to channel -> PRIVMSG #testchan :I'm a message", function (done) { + const server = IRCServer.create() + let mockedSock = new EventEmitter() + mockedSock.address = function () { + return {port: 12346, family: 'IPv4', address: '127.0.0.1'} + } + mockedSock.write = function (data) { + let answer = data.toString("ascii") + if (answer.indexOf("PRIVMSG") >= 0) { + assert.equal(answer, ":some_nick PRIVMSG #testchan :I'm a message\r\n") + done() + } + } + mockedSock.destroy = function () { + done("Destroyed socket without answering") + } + + server.emit("connection", mockedSock) + mockedSock.emit('data', Buffer.from("NICK some_nick\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("USER guest tolmoon tolsun :Ronnie Reagan\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("JOIN #testchan\r\n", "ascii")) + mockedSock.emit('data', Buffer.from("PRIVMSG #testchan :I'm a message\r\n", "ascii")) + }) }) From a80271ec20af85a403d9f3b8e04ada00d1fbc019 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 22:53:31 +0200 Subject: [PATCH 18/19] Improve install instructions --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a5a34a..5254911 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,26 @@ ircd-js A simple IRCd project to provide an IRCd. + +Installing dependencies +=== + +```bash +# Installing docker on (latest) Ubuntu +sudo apt-get install docker.io +``` + Running === +Staring a NodeJS container: + +```bash +docker run -v $PWD:/app --workdir /app -p 127.0.0.1:6667:6667 --rm -it node:10 /bin/bash +``` + +Commands to run inside the container: + ```bash # Install all dev dependencies npm install @@ -14,5 +31,5 @@ npm install npm test # Start the index.js -npm start +node index.js ``` From ca9c0e53ea790a7dffbca27585f42373c8605ceb Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 17 Jul 2019 22:56:00 +0200 Subject: [PATCH 19/19] Release version 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d99e9a0..fb1e22e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ircd", - "version": "0.1.0", + "version": "0.2.0", "description": "A project implementation of the IRC protocol", "main": "index.js", "scripts": {