hardlounge/src/models/network.js

360 lines
8.9 KiB
JavaScript
Raw Normal View History

"use strict";
2018-01-11 11:33:36 +00:00
const _ = require("lodash");
2017-11-28 17:25:15 +00:00
const uuidv4 = require("uuid/v4");
const IrcFramework = require("irc-framework");
2018-01-11 11:33:36 +00:00
const Chan = require("./chan");
const Msg = require("./msg");
const Helper = require("../helper");
2014-09-13 21:29:45 +00:00
module.exports = Network;
let id = 1;
2014-09-13 21:29:45 +00:00
/**
* @type {Object} List of keys which should not be sent to the client.
*/
const filteredFromClient = {
awayMessage: true,
chanCache: true,
highlightRegex: true,
irc: true,
password: true,
};
2014-09-13 21:29:45 +00:00
function Network(attr) {
_.defaults(this, attr, {
2014-10-11 20:44:56 +00:00
name: "",
host: "",
port: 6667,
tls: false,
rejectUnauthorized: false,
2014-10-11 20:44:56 +00:00
password: "",
awayMessage: "",
2014-11-09 16:01:22 +00:00
commands: [],
2014-10-11 20:44:56 +00:00
username: "",
realname: "",
2014-09-13 21:29:45 +00:00
channels: [],
2016-04-03 05:12:49 +00:00
ip: null,
hostname: null,
2014-09-13 21:29:45 +00:00
id: id++,
irc: null,
serverOptions: {
PREFIX: [],
NETWORK: "",
},
2016-05-29 02:07:34 +00:00
chanCache: [],
});
2017-11-28 17:25:15 +00:00
if (!this.uuid) {
this.uuid = uuidv4();
}
2017-07-06 12:02:32 +00:00
if (!this.name) {
this.name = this.host;
}
2014-09-13 21:29:45 +00:00
this.channels.unshift(
2014-10-11 22:47:24 +00:00
new Chan({
name: this.name,
type: Chan.Type.LOBBY,
2014-10-11 22:47:24 +00:00
})
2014-09-13 21:29:45 +00:00
);
}
Network.prototype.validate = function(client) {
this.setNick(String(this.nick || Helper.getDefaultNick()).replace(" ", "_"));
if (!this.username) {
this.username = this.nick.replace(/[^a-zA-Z0-9]/g, "");
}
if (!this.realname) {
this.realname = "The Lounge User";
}
if (!this.port) {
this.port = this.tls ? 6697 : 6667;
}
if (Helper.config.lockNetwork) {
// This check is needed to prevent invalid user configurations
if (!Helper.config.public && this.host && this.host.length > 0 && this.host !== Helper.config.defaults.host) {
this.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "Hostname you specified is not allowed.",
}), true);
return false;
}
this.host = Helper.config.defaults.host;
this.port = Helper.config.defaults.port;
this.tls = Helper.config.defaults.tls;
this.rejectUnauthorized = Helper.config.defaults.rejectUnauthorized;
}
if (this.host.length === 0) {
this.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "You must specify a hostname to connect.",
}), true);
return false;
}
return true;
};
Network.prototype.createIrcFramework = function(client) {
this.irc = new IrcFramework.Client({
version: false, // We handle it ourselves
host: this.host,
port: this.port,
nick: this.nick,
username: Helper.config.useHexIp ? Helper.ip2hex(this.ip) : this.username,
gecos: this.realname,
password: this.password,
tls: this.tls,
outgoing_addr: Helper.config.bind,
rejectUnauthorized: this.rejectUnauthorized,
enable_chghost: true,
enable_echomessage: true,
auto_reconnect: true,
auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little
auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections
webirc: this.createWebIrc(client),
});
this.irc.requestCap([
"znc.in/self-message", // Legacy echo-message for ZNC
]);
// Request only new messages from ZNC if we have sqlite logging enabled
// See http://wiki.znc.in/Playback
if (client.config.log && Helper.config.messageStorage.includes("sqlite")) {
this.irc.requestCap("znc.in/playback");
}
};
Network.prototype.createWebIrc = function(client) {
if (!Helper.config.webirc || !(this.host in Helper.config.webirc)) {
return null;
}
if (!this.ip) {
log.warn(`Cannot find a valid WEBIRC configuration for ${this.nick}!${this.username}@${this.host}`);
return null;
}
if (!this.hostname) {
this.hostname = this.ip;
}
if (Helper.config.webirc[this.host] instanceof Function) {
return Helper.config.webirc[this.host](client, this);
}
return {
password: Helper.config.webirc[this.host],
username: "thelounge",
address: this.ip,
hostname: this.hostname,
};
};
Network.prototype.edit = function(client, args) {
const oldNick = this.nick;
this.nick = args.nick;
this.host = String(args.host || "");
this.name = String(args.name || "") || this.host;
this.port = parseInt(args.port, 10);
this.tls = !!args.tls;
this.rejectUnauthorized = !!args.rejectUnauthorized;
this.password = String(args.password || "");
this.username = String(args.username || "");
this.realname = String(args.realname || "");
// Split commands into an array
this.commands = String(args.commands || "")
.replace(/\r\n|\r|\n/g, "\n")
.split("\n")
.filter((command) => command.length > 0);
// Sync lobby channel name
this.channels[0].name = this.name;
if (!this.validate(client)) {
return;
}
if (this.irc) {
if (this.nick !== oldNick) {
if (this.irc.connection && this.irc.connection.connected) {
// Send new nick straight away
this.irc.raw("NICK", this.nick);
} else {
this.irc.options.nick = this.irc.user.nick = this.nick;
// Update UI nick straight away if IRC is not connected
client.emit("nick", {
network: this.id,
nick: this.nick,
});
}
}
this.irc.options.host = this.host;
this.irc.options.port = this.port;
this.irc.options.password = this.password;
this.irc.options.gecos = this.irc.user.gecos = this.realname;
this.irc.options.tls = this.tls;
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
if (!Helper.config.useHexIp) {
this.irc.options.username = this.irc.user.username = this.username;
}
}
client.save();
};
Network.prototype.destroy = function() {
this.channels.forEach((channel) => channel.destroy());
};
2016-05-12 11:15:38 +00:00
Network.prototype.setNick = function(nick) {
this.nick = nick;
this.highlightRegex = new RegExp(
// Do not match characters and numbers (unless IRC color)
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
// Escape nickname, as it may contain regex stuff
_.escapeRegExp(nick) +
2016-05-12 11:15:38 +00:00
// Do not match characters and numbers
"(?:[^a-z0-9]|$)",
// Case insensitive search
"i"
);
};
/**
* Get a clean clone of this network that will be sent to the client.
* This function performs manual cloning of network object for
* better control of performance and memory usage.
*
* Both of the parameters that are accepted by this function are passed into channels' getFilteredClone call.
*
* @see {@link Chan#getFilteredClone}
*/
Network.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
if (prop === "channels") {
// Channels objects perform their own cloning
newNetwork[prop] = this[prop].map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage));
} else if (!filteredFromClient[prop]) {
// Some properties that are not useful for the client are skipped
newNetwork[prop] = this[prop];
}
return newNetwork;
}, {});
filteredNetwork.status = this.getNetworkStatus();
return filteredNetwork;
};
Network.prototype.getNetworkStatus = function() {
const status = {
connected: false,
secure: false,
};
if (this.irc && this.irc.connection && this.irc.connection.transport) {
const transport = this.irc.connection.transport;
if (transport.socket) {
2018-02-20 08:35:45 +00:00
const isLocalhost = transport.socket.remoteAddress === "127.0.0.1";
const isAuthorized = transport.socket.encrypted && transport.socket.authorized;
status.connected = transport.isConnected();
2018-02-20 08:35:45 +00:00
status.secure = isAuthorized || isLocalhost;
}
}
return status;
2014-10-11 20:44:56 +00:00
};
Network.prototype.addChannel = function(newChan) {
let index = this.channels.length; // Default to putting as the last item in the array
// Don't sort special channels in amongst channels/users.
if (newChan.type === Chan.Type.CHANNEL || newChan.type === Chan.Type.QUERY) {
// We start at 1 so we don't test against the lobby
for (let i = 1; i < this.channels.length; i++) {
const compareChan = this.channels[i];
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
if (newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0
|| (compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)) {
index = i;
break;
}
}
}
this.channels.splice(index, 0, newChan);
return index;
};
2014-10-11 20:44:56 +00:00
Network.prototype.export = function() {
2018-01-11 11:33:36 +00:00
const network = _.pick(this, [
2017-11-28 17:25:15 +00:00
"uuid",
"awayMessage",
2016-05-12 11:15:38 +00:00
"nick",
2014-10-11 22:47:24 +00:00
"name",
"host",
"port",
"tls",
"rejectUnauthorized",
2014-10-11 22:47:24 +00:00
"password",
"username",
2014-11-09 16:01:22 +00:00
"realname",
2016-04-03 05:12:49 +00:00
"commands",
"ip",
"hostname",
2014-10-11 22:47:24 +00:00
]);
2016-05-12 11:15:38 +00:00
2016-06-19 17:12:42 +00:00
network.channels = this.channels
.filter(function(channel) {
2018-01-30 16:46:34 +00:00
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
2016-06-19 17:12:42 +00:00
})
.map(function(chan) {
2018-01-30 16:46:34 +00:00
const keys = ["name"];
2018-01-30 16:46:34 +00:00
if (chan.type === Chan.Type.CHANNEL) {
keys.push("key");
} else if (chan.type === Chan.Type.QUERY) {
keys.push("type");
}
2018-01-30 16:46:34 +00:00
return _.pick(chan, keys);
2016-06-19 17:12:42 +00:00
});
2016-05-12 11:15:38 +00:00
2014-10-11 20:44:56 +00:00
return network;
2014-09-13 21:29:45 +00:00
};
2016-03-20 14:28:47 +00:00
Network.prototype.getChannel = function(name) {
name = name.toLowerCase();
return _.find(this.channels, function(that, i) {
// Skip network lobby (it's always unshifted into first position)
return i > 0 && that.name.toLowerCase() === name;
2016-03-20 14:28:47 +00:00
});
};