Browse Source

Merge release 0.2.0 from develop

master 0.2.0
Sheogorath 5 years ago
parent
commit
57d7a69309
No known key found for this signature in database GPG Key ID: 1F05CC3635CDDFFD
  1. 19
      README.md
  2. 2
      package.json
  3. 32
      src/channel.js
  4. 167
      src/server.js
  5. 29
      src/user.js
  6. 58
      test/channel.js
  7. 72
      test/command_join.js
  8. 4
      test/command_nick.js
  9. 41
      test/command_pass.js
  10. 81
      test/command_privmsg.js
  11. 1
      test/command_quit.js
  12. 84
      test/user.js

19
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
```

2
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": {

32
src/channel.js

@ -0,0 +1,32 @@
function Channel(name) {
this.name = name
this.userlist = []
this.join = function(user) {
let channel = this
this.userlist.push(user)
this.userlist.forEach(function(item) {
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)
}
}
this.sendMsg = function(from, message) {
const channel = this
this.userlist.forEach(function(item) {
item.sendMsg(from, message, channel.name)
})
}
}
module.exports = Channel

167
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'
@ -11,79 +12,111 @@ const ERR_NONICKNAMEGIVEN = '431'
const ERR_NICKNAMEINUSE = '433'
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 channellist = {}
let server = net.createServer((socket) => {
const user = new User(socket)
const user = new User(socket, !config.password)
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])
user.setPassword(tokenized[1])
break;
default:
console.error(`Unknown command: ${command}`);
}
});
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(`:irc.example.com ${ERR_NICKNAMEINUSE} ${nickname} :Nickname is already in use`, 'ascii')
}
} else {
socket.write(`:irc.example.com ${ERR_NONICKNAMEGIVEN} :No nickname given`, 'ascii')
}
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
let message = tokenized[2]
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]
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)
break;
case "PASS":
if (tokenized[1] === config.password) {
user.authenticated = true
} else {
user.closeConnection()
}
break;
default:
console.error(`Unknown command: ${command}`);
}
});
}).on('error', (err) => {
console.error(err);
})

29
src/user.js

@ -1,22 +1,26 @@
function User(socket) {
function User(socket, authenticatedDefault) {
this.registered = false
this.nickname = ""
this.authenticated = authenticatedDefault
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
}
this.getNickname = function(nickname) {
return this.nickname
}
this.setPassword = function (password) {
this.password = password
}
this.register = function(username, realname) {
if (!this.authenticated) {
this.connection.destroy()
}
this.username = username
this.realname = realname
this.registered = true
@ -25,20 +29,17 @@ function User(socket) {
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 () {
socket.destroy()
this.connection.destroy()
}
this.sendMsg = function (from, message) {
this.connection.write(`:${from.nickname} PRIVMSG ${this.nickname} :${message}\r\n`, "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")
}
}

58
test/channel.js

@ -0,0 +1,58 @@
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)
})
})
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)
})
})
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")
})
})
})

72
test/command_join.js

@ -0,0 +1,72 @@
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"))
})
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"))
})
})

4
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 () {

41
test/command_pass.js

@ -4,21 +4,54 @@ 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()
})
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"))
})
})
})

81
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")
@ -62,4 +62,67 @@ describe("PRIVMSG OK", function () {
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"))
})
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"))
})
})

1
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)

84
test/user.js

@ -0,0 +1,84 @@
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, 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()
}}
const user = new User(mockedSock, true)
user.setNickname("some_nick")
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() {
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()
})
})
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")
})
})
})
Loading…
Cancel
Save