Compare commits

...

6 Commits

7 changed files with 338 additions and 394 deletions

View File

@ -1,19 +1,42 @@
# salty-ircd
salty-ircd is an [Internet Relay Chat](https://en.wikipedia.org/wiki/Internet_Relay_Chat) daemon written in [D](https://dlang.org/).
## Goals
The main goals of salty-ircd are strict RFC compliance and security.
### RFC compliance
salty-ircd aims to be fully compliant with the IRC RFCs (in 'RFC mode'), specifically [RFC 1459](https://tools.ietf.org/html/rfc1459), [RFC 2811](https://tools.ietf.org/html/rfc2811), [RFC 2812](https://tools.ietf.org/html/rfc2812), and [RFC 2813](https://tools.ietf.org/html/rfc2813) (planned).
Newer RFCs take precedence over older RFCs. Errata are respected by default, with newer errata (on the same RFC) taking precedence over older errata.
salty-ircd aims to be fully compliant with the IRC RFCs (by default), specifically [RFC 1459](https://tools.ietf.org/html/rfc1459), [RFC 2811](https://tools.ietf.org/html/rfc2811), [RFC 2812](https://tools.ietf.org/html/rfc2812), and [RFC 2813](https://tools.ietf.org/html/rfc2813) (planned), including all errata.
Newer RFCs take precedence over older RFCs.
Any additional features breaking RFC compliance will be made available through compile-time options (i.e. 'non-RFC modes').
Any additional features breaking RFC compliance are available through compile-time options.
### Security (non-RFC modes)
* salty-ircd will require TLS for all connections. An exception could be made to allow hosting a Tor hidden service.
* salty-ircd will require TLS client certificates for authentication.
### Security
The following rules apply when any compile-time option is enabled (breaking strict RFC compliance):
* TLS is required for all connections, except connections from localhost (useful for running a Tor hidden service, which already has encryption)
* TLS client certificates are required for oper and vhost authentication
## Building
Build dependencies:
* A D compiler
* dub
* fish
* git
Build command:
* RFC compliant: `dub build`
* Modern (all additional features): `dub build -c=modern`
The 'modern' configuration aims to be mostly compatible with UnrealIRCd user modes/chars.
When `-d=ProxyV1` is added to the build command, salty-ircd can accept traffic through the PROXY protocol (version 1), e.g. from an UnrealIRCd server running with [a custom module](https://github.com/lesderid/unrealircd5_proxyv1_copy). Please see this module's readme for more information. Note that this is simply a development/debugging option and should not be used for a production server.
TODO: Add a method to supply a custom list of compile-time options.
## Running
First, create the configuration file, `config.sdl`. You can find a template in `config.template.sdl`.
Then, simply run `./out/salty-ircd`.
## License
The source code for salty-ircd is licensed under the [University of Illinois/NCSA Open Source License](LICENSE).
[University of Illinois/NCSA Open Source License](LICENSE).

View File

@ -11,9 +11,9 @@ preBuildCommands "./generate-version-info.fish"
versions "VibeDefaultMain"
targetPath "out"
configuration "application" {
configuration "compliant" {
}
configuration "modern" {
versions "BasicFixes" "SupportTLS"
versions "Modern"
}

View File

@ -8,6 +8,7 @@ import ircd.connection;
import ircd.server;
import ircd.message;
import ircd.helpers;
import ircd.numerics;
//TODO: Make this a struct?
class Channel
@ -96,16 +97,14 @@ class Channel
auto onChannel = members.canFind(connection);
connection.send(Message(_server.name, "353", [
connection.nick, channelType, name,
members.filter!(m => onChannel || !m.modes.canFind('i'))
.map!(m => prefixedNick(m))
.join(' ')
], true));
connection.sendNumeric!RPL_NAMREPLY(channelType, name,
members.filter!(m => onChannel || !m.modes.canFind('i'))
.map!(m => prefixedNick(m))
.join(' '));
if (sendRplEndOfNames)
{
connection.sendRplEndOfNames(name);
connection.sendNumeric!RPL_ENDOFNAMES(name);
}
}
@ -128,17 +127,9 @@ class Channel
void sendTopic(Connection connection)
{
if (topic.empty)
{
connection.send(Message(_server.name, "331", [
connection.nick, name, "No topic is set"
]));
}
connection.sendNumeric!RPL_NOTOPIC(name);
else
{
connection.send(Message(_server.name, "332", [
connection.nick, name, topic
], true));
}
connection.sendNumeric!RPL_TOPIC(name, topic);
}
void setTopic(Connection connection, string newTopic)
@ -183,9 +174,8 @@ class Channel
specialModeParameters ~= memberLimit.to!string;
}
user.send(Message(_server.name, "324", [
user.nick, name, "+" ~ modes.idup ~ specialModes
] ~ specialModeParameters));
user.sendNumeric!RPL_CHANNELMODEIS([name,
"+" ~ modes.idup ~ specialModes] ~ specialModeParameters);
}
bool setMemberMode(Connection target, char mode)
@ -280,42 +270,30 @@ class Channel
{
foreach (entry; maskLists['b'])
{
connection.send(Message(_server.name, "367", [
connection.nick, name, entry
], false));
connection.sendNumeric!RPL_BANLIST(name, entry);
}
connection.send(Message(_server.name, "368", [
connection.nick, name, "End of channel ban list"
], true));
connection.sendNumeric!RPL_ENDOFBANLIST(name);
}
void sendExceptList(Connection connection)
{
foreach (entry; maskLists['e'])
{
connection.send(Message(_server.name, "348", [
connection.nick, name, entry
], false));
connection.sendNumeric!RPL_EXCEPTLIST(name, entry);
}
connection.send(Message(_server.name, "349", [
connection.nick, name, "End of channel exception list"
], true));
connection.sendNumeric!RPL_ENDOFEXCEPTLIST(name);
}
void sendInviteList(Connection connection)
{
foreach (entry; maskLists['I'])
{
connection.send(Message(_server.name, "346", [
connection.nick, name, entry
], false));
connection.sendNumeric!RPL_INVITELIST(name, entry);
}
connection.send(Message(_server.name, "347", [
connection.nick, name, "End of channel invite list"
], true));
connection.sendNumeric!RPL_ENDOFINVITELIST(name);
}
bool setKey(string key)

View File

@ -11,6 +11,7 @@ import std.datetime;
import vibe.core.core;
import vibe.core.net;
import vibe.core.stream : IOMode;
import vibe.stream.operations : readLine;
import ircd.versionInfo;
@ -19,6 +20,7 @@ import ircd.message;
import ircd.server;
import ircd.channel;
import ircd.helpers;
import ircd.numerics;
//TODO: Make this a struct?
class Connection
@ -40,6 +42,9 @@ class Connection
string pass = null;
debug (ProxyV1)
bool proxy;
@property auto channels()
{
return _server.channels.filter!(c => c.members.canFind(this));
@ -121,7 +126,21 @@ class Connection
{
string messageString = message.toString;
writeln("S> " ~ messageString);
_connection.write(messageString ~ "\r\n");
auto messageBytes = cast(const(ubyte)[]) (messageString ~ "\r\n");
auto bytesSent = _connection.write(messageBytes, IOMode.once);
if (bytesSent < 0)
{
writeln("client disconnected (write error)");
closeConnection();
}
}
void sendNumeric(alias numeric)(string[] params...)
{
auto message = Message(_server.name, numeric.number, [nick] ~ params ~ numeric.params);
send(message);
}
void closeConnection()
@ -167,10 +186,19 @@ class Connection
continue;
}
debug (ProxyV1)
if (message.command == "PROXY")
{
proxy = true;
hostname = getHost(message.parameters[1]);
}
//NOTE: The RFCs don't specify whether commands are case-sensitive
version (BasicFixes)
{
message.command = message.command.map!toUpper.to!string;
message.command = message.command
.map!toUpper
.to!string;
}
//NOTE: The RFCs don't specify what 'being idle' means
@ -185,7 +213,10 @@ class Connection
if (!registered && !["NICK", "USER", "PASS", "PING", "PONG",
"QUIT"].canFind(message.command))
{
sendErrNotRegistered();
//NOTE: This actually does not work if NICK hasn't been sent
// The first parameter for numerics is the client's nick.
// This makes it technically impossible to correctly implement the RFCs.
sendNumeric!ERR_NOTREGISTERED();
continue;
}
@ -274,23 +305,21 @@ class Connection
break;
default:
writeln("unknown command '", message.command, "'");
send(Message(_server.name, "421", [
nick, message.command, "Unknown command"
]));
sendNumeric!ERR_UNKNOWNCOMMAND();
continue;
}
_server.updateCommandStatistics(message);
}
onDisconnect();
closeConnection();
}
void onNick(Message message)
{
if (message.parameters.length == 0)
{
sendErrNoNickGiven();
sendNumeric!ERR_NONICKNAMEGIVEN();
return;
}
@ -298,17 +327,13 @@ class Connection
if (!_server.isNickAvailable(newNick) && newNick.toIRCLower != nick.toIRCLower)
{
send(Message(_server.name, "433", [
nick, newNick, "Nickname is already in use"
]));
sendNumeric!ERR_NICKNAMEINUSE(newNick);
return;
}
if (!_server.isValidNick(newNick))
{
send(Message(_server.name, "432", [
nick, newNick, "Erroneous nickname"
]));
sendNumeric!ERR_ERRONEUSNICKNAME(newNick);
return;
}
@ -326,7 +351,7 @@ class Connection
{
sendWelcome();
}
else if (registrationAttempted)
else if (!wasRegistered && registrationAttempted)
{
onIncorrectPassword();
}
@ -336,15 +361,13 @@ class Connection
{
if (message.parameters.length < 4)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
if (user !is null)
{
send(Message(_server.name, "462", [
nick, "Unauthorized command (already registered)"
], true));
sendNumeric!ERR_ALREADYREGISTRED();
return;
}
@ -354,7 +377,8 @@ class Connection
user = message.parameters[0];
modes = modeMaskToModes(message.parameters[1]);
realname = message.parameters[3];
hostname = getHost();
debug (ProxyV1) {}
else hostname = getHost();
if (!wasRegistered && registered)
{
@ -370,15 +394,13 @@ class Connection
{
if (message.parameters.length < 1)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
if (registered)
{
send(Message(_server.name, "462", [
nick, "Unauthorized command (already registered)"
], true));
sendNumeric!ERR_ALREADYREGISTRED();
return;
}
@ -404,7 +426,7 @@ class Connection
{
if (message.parameters.length == 0)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -423,7 +445,7 @@ class Connection
{
if (!Server.isValidChannelName(channelName))
{
sendErrNoSuchChannel(channelName);
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
}
else
{
@ -444,32 +466,24 @@ class Connection
if (!channel.memberLimit.isNull
&& channel.members.length >= channel.memberLimit.get)
{
send(Message(_server.name, "471", [
nick, channelName, "Cannot join channel (+l)"
]));
sendNumeric!ERR_CHANNELISFULL(channelName);
}
else if (channel.modes.canFind('i')
&& !(channel.maskLists['I'].any!(m => matchesMask(m))
|| channel.inviteHolders.canFind(this)))
{
send(Message(_server.name, "473", [
nick, channelName, "Cannot join channel (+i)"
]));
sendNumeric!ERR_INVITEONLYCHAN(channelName);
}
else if (channel.maskLists['b'].any!(m => matchesMask(m))
&& !channel.maskLists['e'].any!(m => matchesMask(m))
&& !channel.inviteHolders.canFind(this))
{
send(Message(_server.name, "474", [
nick, channelName, "Cannot join channel (+b)"
]));
sendNumeric!ERR_BANNEDFROMCHAN(channelName);
}
else if (channel.key !is null && (channelKeys.length < i + 1
|| channelKeys[i] != channel.key))
{
send(Message(_server.name, "475", [
nick, channelName, "Cannot join channel (+k)"
]));
sendNumeric!ERR_BADCHANNELKEY(channelName);
}
else
{
@ -484,7 +498,7 @@ class Connection
{
if (message.parameters.length == 0)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -493,25 +507,19 @@ class Connection
{
if (!Server.isValidChannelName(channel))
{
sendErrNoSuchChannel(channel);
sendNumeric!ERR_NOSUCHCHANNEL(channel);
}
else if (!_server.canFindChannelByName(channel)
|| !channels.canFind!(c => c.name.toIRCLower == channel.toIRCLower))
{
send(Message(_server.name, "442", [
nick, channel, "You're not on that channel"
], true));
sendNumeric!ERR_NOTONCHANNEL(channel);
}
else
{
if (message.parameters.length > 1)
{
_server.part(this, channel, message.parameters[1]);
}
else
{
_server.part(this, channel, null);
}
}
}
}
@ -524,14 +532,12 @@ class Connection
if (message.parameters.length == 0)
{
send(Message(_server.name, "411", [
nick, "No recipient given (PRIVMSG)"
], true));
sendNumeric!ERR_NORECIPIENT_PRIVMSG();
return;
}
if (message.parameters.length == 1)
{
send(Message(_server.name, "412", [nick, "No text to send"], true));
sendNumeric!ERR_NOTEXTTOSEND();
return;
}
@ -540,13 +546,11 @@ class Connection
auto channelRange = _server.findChannelByName(target);
if (channelRange.empty)
{
sendErrNoSuchNick(target);
sendNumeric!ERR_NOSUCHNICK(target);
}
else if (!channelRange[0].canReceiveMessagesFromUser(this))
{
send(Message(_server.name, "404", [
nick, target, "Cannot send to channel"
], true));
sendNumeric!ERR_CANNOTSENDTOCHAN(target);
}
else
{
@ -557,7 +561,7 @@ class Connection
{
if (!_server.canFindConnectionByNick(target))
{
sendErrNoSuchNick(target);
sendNumeric!ERR_NOSUCHNICK(target);
}
else
{
@ -566,30 +570,35 @@ class Connection
auto targetUser = _server.findConnectionByNick(target)[0];
if (targetUser.modes.canFind('a'))
{
sendRplAway(target, targetUser.awayMessage);
sendNumeric!RPL_AWAY(target, targetUser.awayMessage);
}
}
}
else
{
//is this the right reply?
sendErrNoSuchNick(target);
sendNumeric!ERR_NOSUCHNICK(target);
}
}
void onNotice(Message message)
{
//TODO: Support special message targets
auto target = message.parameters[0];
auto text = message.parameters[1];
//TODO: Figure out what we are allowed to send exactly
if (message.parameters.length < 2)
if (message.parameters.length == 0)
{
sendNumeric!ERR_NORECIPIENT_NOTICE();
return;
}
auto target = message.parameters[0];
if (message.parameters.length == 1)
{
sendNumeric!ERR_NOTEXTTOSEND();
return;
}
auto text = message.parameters[1];
//TODO: Fix replies
if (Server.isValidChannelName(target) && _server.canFindChannelByName(target))
{
_server.noticeToChannel(this, target, text);
@ -622,7 +631,7 @@ class Connection
}
auto name = message.parameters.length == 0 ? "*" : message.parameters[0];
send(Message(_server.name, "315", [nick, name, "End of WHO list"], true));
sendNumeric!RPL_ENDOFWHO(name);
}
void onAway(Message message)
@ -631,17 +640,13 @@ class Connection
{
removeMode('a');
awayMessage = null;
send(Message(_server.name, "305", [
nick, "You are no longer marked as being away"
], true));
sendNumeric!RPL_UNAWAY();
}
else
{
modes ~= 'a';
awayMessage = message.parameters[0];
send(Message(_server.name, "306", [
nick, "You have been marked as being away"
], true));
sendNumeric!RPL_NOWAWAY();
}
}
@ -649,7 +654,7 @@ class Connection
{
if (message.parameters.length == 0)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -661,15 +666,9 @@ class Connection
{
//NOTE: The RFCs don't allow ERR_NOSUCHCHANNEL as a response to TOPIC
version (BasicFixes)
{
sendErrNoSuchChannel(channelName);
}
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
else
{
send(Message(_server.name, "331", [
nick, channelName, "No topic is set"
], true));
}
sendNumeric!RPL_NOTOPIC(channelName);
}
else
{
@ -681,13 +680,13 @@ class Connection
auto newTopic = message.parameters[1];
if (!channels.canFind!(c => c.name.toIRCLower == channelName.toIRCLower))
{
sendErrNotOnChannel(channelName);
sendNumeric!ERR_NOTONCHANNEL(channelName);
}
else if (channels.find!(c => c.name.toIRCLower == channelName.toIRCLower)
.map!(c => c.modes.canFind('t') && !c.memberModes[this].canFind('o'))
.array[0])
{
sendErrChanopPrivsNeeded(channelName);
sendNumeric!ERR_CHANOPRIVSNEEDED(channelName);
}
else
{
@ -719,7 +718,7 @@ class Connection
}
else
{
sendRplEndOfNames(channelName);
sendNumeric!RPL_ENDOFNAMES(channelName);
}
}
}
@ -748,7 +747,7 @@ class Connection
{
if (message.parameters.length < 2)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -756,7 +755,7 @@ class Connection
auto targetUserRange = _server.findConnectionByNick(targetNick);
if (targetUserRange.empty)
{
sendErrNoSuchNick(targetNick);
sendNumeric!ERR_NOSUCHNICK(targetNick);
return;
}
auto targetUser = targetUserRange[0];
@ -767,11 +766,11 @@ class Connection
{
_server.invite(this, targetUser.nick, channelName);
sendRplInviting(channelName, targetUser.nick);
sendNumeric!RPL_INVITING(targetUser.nick, channelName);
if (targetUser.modes.canFind('a'))
{
sendRplAway(targetUser.nick, targetUser.awayMessage);
sendNumeric!RPL_AWAY(targetUser.nick, targetUser.awayMessage);
}
}
else
@ -779,28 +778,25 @@ class Connection
auto channel = channelRange[0];
if (!channel.members.canFind(this))
{
sendErrNotOnChannel(channel.name);
sendNumeric!ERR_NOTONCHANNEL(channel.name);
}
else if (channel.members.canFind(targetUser))
{
send(Message(_server.name, "443", [
nick, targetUser.nick, channel.name,
"is already on channel"
], true));
sendNumeric!ERR_USERONCHANNEL(targetUser.nick, channel.name);
}
else if (channel.modes.canFind('i') && !channel.memberModes[this].canFind('o'))
{
sendErrChanopPrivsNeeded(channel.name);
sendNumeric!ERR_CHANOPRIVSNEEDED(channel.name);
}
else
{
_server.invite(this, targetUser.nick, channel.name);
sendRplInviting(channel.name, targetUser.nick);
sendNumeric!RPL_INVITING(targetUser.nick, channel.name);
if (targetUser.modes.canFind('a'))
{
sendRplAway(targetUser.nick, targetUser.awayMessage);
sendNumeric!RPL_AWAY(targetUser.nick, targetUser.awayMessage);
}
}
}
@ -835,7 +831,7 @@ class Connection
}
else if (_server.motd is null)
{
send(Message(_server.name, "422", [nick, "MOTD File is missing"], true));
sendNumeric!ERR_NOMOTD();
return;
}
_server.sendMotd(this);
@ -860,7 +856,7 @@ class Connection
{
if (message.parameters.length < 1)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -876,7 +872,7 @@ class Connection
{
if (message.parameters.length < 1)
{
sendErrNoNickGiven();
sendNumeric!ERR_NONICKNAMEGIVEN();
return;
}
else if (message.parameters.length > 1)
@ -890,34 +886,34 @@ class Connection
if (!_server.canFindConnectionByNick(mask)
|| !_server.findConnectionByNick(mask)[0].visibleTo(this))
{
sendErrNoSuchNick(mask);
sendNumeric!ERR_NOSUCHNICK(mask);
}
else
{
_server.whois(this, mask);
}
send(Message(_server.name, "318", [nick, mask, "End of WHOIS list"], true));
sendNumeric!RPL_ENDOFWHOIS(mask);
}
void onKill(Message message)
{
if (!isOperator)
{
sendErrNoPrivileges();
sendNumeric!ERR_NOPRIVILEGES();
return;
}
if (message.parameters.length < 2)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
auto nick = message.parameters[0];
if (!_server.canFindConnectionByNick(nick))
{
sendErrNoSuchNick(nick);
sendNumeric!ERR_NOSUCHNICK(nick);
return;
}
@ -930,7 +926,7 @@ class Connection
{
if (message.parameters.length < 2)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -940,7 +936,7 @@ class Connection
if (channelList.length != 1 && channelList.length != userList.length)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -954,22 +950,22 @@ class Connection
if (!_server.canFindChannelByName(channelName))
{
sendErrNoSuchChannel(channelName);
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
}
else
{
auto channel = _server.findChannelByName(channelName)[0];
if (!channel.members.canFind(this))
{
sendErrNotOnChannel(channelName);
sendNumeric!ERR_NOTONCHANNEL(channelName);
}
else if (!channel.memberModes[this].canFind('o'))
{
sendErrChanopPrivsNeeded(channelName);
sendNumeric!ERR_CHANOPRIVSNEEDED(channelName);
}
else if (!channel.members.canFind!(m => m.nick.toIRCLower == nick.toIRCLower))
{
sendErrUserNotInChannel(nick, channelName);
sendNumeric!ERR_USERNOTINCHANNEL(nick, channelName);
}
else
{
@ -983,7 +979,7 @@ class Connection
{
if (message.parameters.empty)
{
sendErrNeedMoreParams(message.command);
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
return;
}
@ -1001,7 +997,7 @@ class Connection
//NOTE: The RFCs don't allow ERR_NOSUCHNICK as a reponse to MODE
version (BasicFixes)
{
sendErrNoSuchNick(target);
sendNumeric!ERR_NOSUCHNICK(target);
}
}
}
@ -1016,24 +1012,20 @@ class Connection
version (BasicFixes)
{
if (message.parameters.length > 1)
{
send(Message(_server.name, "502", [nick, "Cannot change mode for other users"], true));
}
sendNumeric!ERR_USERSDONTMATCH();
else
{
send(Message(_server.name, "502", [nick, "Cannot view mode of other users"], true));
}
sendNumeric!ERR_USERSDONTMATCH_ALT();
}
else
{
send(Message(_server.name, "502", [nick, "Cannot change mode for other users"], true));
sendNumeric!ERR_USERSDONTMATCH();
}
return;
}
if (message.parameters.length == 1)
{
send(Message(_server.name, "221", [nick, "+" ~ modes.idup]));
sendNumeric!RPL_UMODEIS("+" ~ modes.idup);
}
else
{
@ -1045,15 +1037,14 @@ class Connection
//NOTE: The RFCs don't specify what should happen on malformed mode operations
version (BasicFixes)
{
sendMalformedMessageError(message.command, "Invalid mode operation: " ~ modeString[0]);
sendMalformedMessageError(message.command,
"Invalid mode operation: " ~ modeString[0]);
}
continue;
}
if (modeString.length == 1)
{
continue;
}
auto changedModes = modeString[1 .. $];
foreach (mode; changedModes)
@ -1081,9 +1072,7 @@ class Connection
//ignore
break;
default:
send(Message(_server.name, "501", [
nick, "Unknown MODE flag"
], true));
sendNumeric!ERR_UMODEUNKNOWNFLAG();
break;
}
}
@ -1099,7 +1088,7 @@ class Connection
//NOTE: The RFCs don't allow ERR_NOSUCHCHANNEL as a response to MODE
version (BasicFixes)
{
sendErrNoSuchChannel(message.parameters[0]);
sendNumeric!ERR_NOSUCHCHANNEL(message.parameters[0]);
}
return;
}
@ -1108,13 +1097,9 @@ class Connection
//NOTE: The RFCs are inconsistent on channel mode query syntax for mask list modes
// ('MODE #chan +b', but 'MODE #chan e' and 'MODE #chan I')
version (BasicFixes)
{
enum listQueryModes = ["+b", "+e", "+I", "e", "I"];
}
else
{
enum listQueryModes = ["+b", "e", "I"];
}
if (message.parameters.length == 1)
{
@ -1140,7 +1125,7 @@ class Connection
{
if (!channel.memberModes[this].canFind('o'))
{
sendErrChanopPrivsNeeded(channel.name);
sendNumeric!ERR_CHANOPRIVSNEEDED(channel.name);
return;
}
@ -1153,15 +1138,14 @@ class Connection
//NOTE: The RFCs don't specify what should happen on malformed mode operations
version (BasicFixes)
{
sendMalformedMessageError(message.command, "Invalid mode operation: " ~ modeString[0]);
sendMalformedMessageError(message.command,
"Invalid mode operation: " ~ modeString[0]);
}
return;
}
if (modeString.length == 1)
{
continue;
}
char[] processedModes;
string[] processedParameters;
@ -1174,7 +1158,7 @@ class Connection
//TODO: If RFC-strictness is on, limit mode changes with parameter to 3 per command
case 'o':
case 'v':
case 'v':
if (i + 1 == message.parameters.length)
{
break Lforeach;
@ -1184,14 +1168,14 @@ class Connection
auto memberRange = _server.findConnectionByNick(memberNick);
if (memberRange.empty)
{
sendErrNoSuchNick(memberNick);
sendNumeric!ERR_NOSUCHNICK(memberNick);
break Lforeach;
}
auto member = memberRange[0];
if (!channel.members.canFind(member))
{
sendErrUserNotInChannel(memberNick, channel.name);
sendNumeric!ERR_USERNOTINCHANNEL(memberNick, channel.name);
break Lforeach;
}
@ -1226,7 +1210,8 @@ class Connection
}
else
{
sendMalformedMessageError(message.command, "Invalid user mask: " ~ mask);
sendMalformedMessageError(message.command,
"Invalid user mask: " ~ mask);
break Lforeach;
}
}
@ -1269,20 +1254,14 @@ class Connection
if (add)
{
if (i + 1 == message.parameters.length)
{
break Lforeach;
}
auto limitString = message.parameters[++i];
uint limit = 0;
try
{
limit = limitString.to!uint;
}
catch (Throwable)
{
break Lforeach;
}
channel.setMemberLimit(limit);
@ -1298,26 +1277,23 @@ class Connection
}
break;
case 'i':
case 'm':
case 'n':
case 'p':
case 's':
case 't':
case 'm':
case 'n':
case 'p':
case 's':
case 't':
bool success;
if (add)
success = channel.setMode(mode);
else
success = channel.unsetMode(mode);
if (success)
{
processedModes ~= mode;
}
break;
default:
send(Message(_server.name, "472", [
nick, [mode],
"is unknown mode char to me for " ~ channel.name
], true));
sendNumeric!ERR_UNKNOWNMODE([mode],
"is unknown mode char to me for " ~ channel.name);
break;
}
}
@ -1363,13 +1339,9 @@ class Connection
break;
}
send(Message(_server.name, "219", [
nick, [statsLetter].idup, "End of STATS report"
], true));
sendNumeric!RPL_ENDOFSTATS([statsLetter].idup);
}
//TODO: Refactor numeric replies
void sendWhoReply(string channel, Connection user, string nickPrefix, uint hopCount)
{
auto flags = user.modes.canFind('a') ? "G" : "H";
@ -1377,95 +1349,8 @@ class Connection
flags ~= "*";
flags ~= nickPrefix;
send(Message(_server.name, "352", [
nick, channel, user.user, user.hostname, user.servername,
user.nick, flags, hopCount.to!string ~ " " ~ user.realname
], true));
}
void sendRplAway(string target, string message)
{
send(Message(_server.name, "301", [nick, target, message], true));
}
void sendRplList(string channelName, ulong visibleCount, string topic)
{
send(Message(_server.name, "322", [
nick, channelName, visibleCount.to!string, topic
], true));
}
void sendRplListEnd()
{
send(Message(_server.name, "323", [nick, "End of LIST"], true));
}
void sendRplInviting(string channelName, string name)
{
//TODO: If errata are being ignored, send nick and channel name in reverse order
send(Message(_server.name, "341", [nick, name, channelName]));
}
void sendRplEndOfNames(string channelName)
{
send(Message(_server.name, "366", [
nick, channelName, "End of NAMES list"
], true));
}
void sendErrNoSuchNick(string name)
{
send(Message(_server.name, "401", [nick, name, "No such nick/channel"], true));
}
void sendErrNoSuchChannel(string name)
{
send(Message(_server.name, "403", [nick, name, "No such channel"], true));
}
void sendErrNoNickGiven()
{
send(Message(_server.name, "431", [nick, "No nickname given"], true));
}
void sendErrUserNotInChannel(string otherNick, string channel)
{
send(Message(_server.name, "441", [
nick, otherNick, channel, "They aren't on that channel"
], true));
}
void sendErrNotOnChannel(string channel)
{
send(Message(_server.name, "442", [
nick, channel, "You're not on that channel"
], true));
}
void sendErrNotRegistered()
{
send(Message(_server.name, "451", ["(You)", "You have not registered"], true));
}
void sendErrNeedMoreParams(string command)
{
send(Message(_server.name, "461", [
nick, command, "Not enough parameters"
], true));
}
void sendErrNoPrivileges()
{
send(Message(_server.name, "481", [
nick, "Permission Denied- You're not an IRC operator"
], true));
}
void sendErrChanopPrivsNeeded(string channel)
{
send(Message(_server.name, "482", [
nick, channel, "You're not channel operator"
], true));
sendNumeric!RPL_WHOREPLY(channel, user.user, user.hostname, user.servername,
user.nick, flags, hopCount.to!string ~ " " ~ user.realname);
}
void notImplemented(string description)
@ -1477,7 +1362,9 @@ class Connection
void sendMalformedMessageError(string command, string description)
{
send(Message(_server.name, "ERROR", [command, "Malformed message: " ~ description], true));
send(Message(_server.name, "ERROR", [
command, "Malformed message: " ~ description
], true));
}
void sendWelcome()
@ -1487,35 +1374,29 @@ class Connection
enum availableUserModes = "aiwroOs";
enum availableChannelModes = "OovaimnqpsrtklbeI";
send(Message(_server.name, "001", [nick,
"Welcome", "to", "the", "Internet", "Relay", "Network", prefix
], false));
send(Message(_server.name, "002", [nick,
"Your", "host", "is", _server.name ~ ",", "running", "version", _server.versionString
], false));
send(Message(_server.name, "003", [nick,
"This", "server", "was", "created", buildDate
], false));
send(Message(_server.name, "004", [nick,
_server.name, _server.versionString, availableUserModes, availableChannelModes
], false));
sendNumeric!RPL_WELCOME("Welcome", "to", "the", "Internet", "Relay", "Network", prefix);
sendNumeric!RPL_YOURHOST("Your", "host", "is", _server.name ~ ",",
"running", "version", _server.versionString);
sendNumeric!RPL_CREATED("This", "server", "was", "created", buildDate);
sendNumeric!RPL_MYINFO(_server.name, _server.versionString,
availableUserModes, availableChannelModes);
}
void onIncorrectPassword()
{
//NOTE: The RFCs don't allow ERR_PASSWDMISMATCH as a response to NICK/USER
version (BasicFixes)
{
send(Message(_server.name, "464", [nick, "Password incorrect"], true));
}
sendNumeric!ERR_PASSWDMISMATCH();
//NOTE: The RFCs don't actually specify what should happen here
closeConnection();
}
string getHost()
string getHost(string addressString = null)
{
auto address = parseAddress(_connection.remoteAddress.toAddressString);
auto address = parseAddress(addressString != null ?
addressString :
_connection.remoteAddress.toAddressString);
auto hostname = address.toHostNameString;
if (hostname is null)
{
@ -1536,13 +1417,10 @@ class Connection
char[] modes;
if (mask & 0b100)
{
modes ~= 'w';
}
if (mask & 0b1000)
{
modes ~= 'i';
}
return modes;
}

89
source/ircd/numerics.d Normal file
View File

@ -0,0 +1,89 @@
module ircd.numerics;
struct SimpleNumeric
{
string number;
string[] params;
}
private alias N = SimpleNumeric;
enum : SimpleNumeric
{
//Command responses
RPL_WELCOME = N("001", []),
RPL_YOURHOST = N("002", []),
RPL_CREATED = N("003", []),
RPL_MYINFO = N("004", []),
RPL_STATSCOMMANDS = N("212", []),
RPL_ENDOFSTATS = N("219", ["End of STATS report"]),
RPL_UMODEIS = N("221", []),
RPL_STATSUPTIME = N("242", []),
RPL_LUSERCLIENT = N("251", []),
RPL_LUSEROP = N("252", ["operator(s) online"]),
RPL_LUSERUNKNOWN = N("253", ["unknown connection(s)"]),
RPL_LUSERCHANNELS = N("254", ["channels formed"]),
RPL_LUSERME = N("255", []),
RPL_AWAY = N("301", []),
RPL_ISON = N("303", []),
RPL_UNAWAY = N("305", ["You are no longer marked as being away"]),
RPL_NOWAWAY = N("306", ["You have been marked as being away"]),
RPL_WHOISUSER = N("311", []),
RPL_WHOISSERVER = N("312", []),
RPL_WHOISOPERATOR = N("313", ["is an IRC operator"]),
RPL_ENDOFWHO = N("315", ["End of WHO list"]),
RPL_WHOISIDLE = N("317", ["seconds idle"]),
RPL_ENDOFWHOIS = N("318", ["End of WHOIS list"]),
RPL_WHOISCHANNELS = N("319", []),
RPL_LIST = N("322", []),
RPL_LISTEND = N("323", ["End of LIST"]),
RPL_CHANNELMODEIS = N("324", []),
RPL_NOTOPIC = N("331", ["No topic is set"]),
RPL_TOPIC = N("332", []),
RPL_INVITING = N("341", []),
RPL_INVITELIST = N("346", []),
RPL_ENDOFINVITELIST = N("347", ["End of channel invite list"]),
RPL_EXCEPTLIST = N("348", []),
RPL_ENDOFEXCEPTLIST = N("349", ["End of channel exception list"]),
RPL_VERSION = N("351", []),
RPL_WHOREPLY = N("352", []),
RPL_NAMREPLY = N("353", []),
RPL_ENDOFNAMES = N("366", ["End of NAMES list"]),
RPL_BANLIST = N("367", []),
RPL_ENDOFBANLIST = N("368", ["End of channel ban list"]),
RPL_MOTD = N("372", []),
RPL_MOTDSTART = N("375", []),
RPL_ENDOFMOTD = N("376", ["End of MOTD command"]),
RPL_TIME = N("391", []),
//Error replies
ERR_NOSUCHNICK = N("401", ["No such nick/channel"]),
ERR_NOSUCHCHANNEL = N("403", ["No such channel"]),
ERR_CANNOTSENDTOCHAN = N("404", ["Cannot send to channel"]),
ERR_NORECIPIENT_PRIVMSG = N("411", ["No recipient given (PRIVMSG)"]),
ERR_NORECIPIENT_NOTICE = N("411", ["No recipient given (NOTICE)"]),
ERR_NOTEXTTOSEND = N("412", ["No text to send"]),
ERR_UNKNOWNCOMMAND = N("421", ["Unknown command"]),
ERR_NOMOTD = N("422", ["MOTD File is missing"]),
ERR_NONICKNAMEGIVEN = N("431", ["No nickname given"]),
ERR_ERRONEUSNICKNAME = N("432", ["Erroneous nickname"]),
ERR_NICKNAMEINUSE = N("433", ["Nickname is already in use"]),
ERR_USERNOTINCHANNEL = N("441", ["They aren't on that channel"]),
ERR_NOTONCHANNEL = N("442", ["You're not on that channel"]),
ERR_USERONCHANNEL = N("443", ["is already on channel"]),
ERR_NOTREGISTERED = N("451", ["You have not registered"]),
ERR_NEEDMOREPARAMS = N("461", ["Not enough parameters"]),
ERR_ALREADYREGISTRED /* sic */ = N("462", ["Unauthorized command (already registered)"]),
ERR_PASSWDMISMATCH = N("464", ["Password incorrect"]),
ERR_CHANNELISFULL = N("471", ["Cannot join channel (+l)"]),
ERR_UNKNOWNMODE = N("472", []),
ERR_INVITEONLYCHAN = N("473", ["Cannot join channel (+i)"]),
ERR_BANNEDFROMCHAN = N("474", ["Cannot join channel (+b)"]),
ERR_BADCHANNELKEY = N("475", ["Cannot join channel (+k)"]),
ERR_NOPRIVILEGES = N("481", ["Permission Denied- You're not an IRC operator"]),
ERR_CHANOPRIVSNEEDED = N("482", ["You're not channel operator"]),
ERR_UMODEUNKNOWNFLAG = N("501", ["Unknown MODE flag"]),
ERR_USERSDONTMATCH = N("502", ["Cannot change mode for other users"]),
ERR_USERSDONTMATCH_ALT = N("502", ["Cannot view mode of other users"]), //non-standard message (NotStrict-only)
}

View File

@ -18,6 +18,7 @@ import ircd.message;
import ircd.connection;
import ircd.channel;
import ircd.helpers;
import ircd.numerics;
//TODO: Make this a struct?
class Server
@ -229,8 +230,8 @@ class Server
{
foreach (c; connections.filter!(c => c.visibleTo(origin))
.filter!(c => !operatorsOnly || c.isOperator)
.filter!(c => [c.hostname, c.servername, c.realname, c.nick]
.any!(n => wildcardMatch(n, mask))))
.filter!(c => [c.hostname, c.servername, c.realname,
c.nick].any!(n => wildcardMatch(n, mask))))
{
//TODO: Don't leak secret/private channels if RFC-strictness is off (the RFCs don't seem to say anything about it?)
auto channelName = c.channels.empty ? "*" : c.channels.array[0].name;
@ -294,24 +295,23 @@ class Server
&& !ch.modes.canFind('p')).empty);
if (!otherUsers.empty)
{
connection.send(Message(name, "353", [
connection.nick, "=", "*",
otherUsers.map!(m => m.nick).join(' ')
], true));
connection.sendNumeric!RPL_NAMREPLY("=", "*", otherUsers.map!(m => m.nick).join(' '));
}
connection.sendRplEndOfNames("*");
connection.sendNumeric!RPL_ENDOFNAMES("*");
}
void sendFullList(Connection connection)
{
foreach (channel; channels.filter!(c => c.visibleTo(connection)))
{
connection.sendRplList(channel.name,
channel.members.filter!(m => m.visibleTo(connection))
.array.length, channel.topic);
connection.sendNumeric!RPL_LIST(channel.name, channel.members
.filter!(m => m.visibleTo(connection))
.array
.length
.to!string, channel.topic);
}
connection.sendRplListEnd();
connection.sendNumeric!RPL_LISTEND();
}
void sendPartialList(Connection connection, string[] channelNames)
@ -319,24 +319,25 @@ class Server
foreach (channel; channels.filter!(c => channelNames.canFind(c.name)
&& c.visibleTo(connection)))
{
connection.sendRplList(channel.name,
channel.members.filter!(m => m.visibleTo(connection))
.array.length, channel.topic);
connection.sendNumeric!RPL_LIST(channel.name, channel.members
.filter!(m => m.visibleTo(connection))
.array
.length
.to!string, channel.topic);
}
connection.sendRplListEnd();
connection.sendNumeric!RPL_LISTEND();
}
void sendVersion(Connection connection)
{
connection.send(Message(name, "351", [
connection.nick, versionString ~ ".", name, ""
], true));
//TODO: Include enabled versions in comments?
connection.sendNumeric!RPL_VERSION(versionString ~ ".", name, ":");
}
void sendTime(Connection connection)
{
auto timeString = Clock.currTime.toISOExtString;
connection.send(Message(name, "391", [connection.nick, name, timeString], true));
connection.sendNumeric!RPL_TIME(name, timeString);
}
void invite(Connection inviter, string target, string channelName)
@ -351,17 +352,13 @@ class Server
void sendMotd(Connection connection)
{
connection.send(Message(name, "375", [
connection.nick, ":- " ~ name ~ " Message of the day - "
], true));
connection.sendNumeric!RPL_MOTDSTART("- " ~ name ~ " Message of the day - ");
foreach (line; motd.splitLines)
{
//TODO: Implement line wrapping
connection.send(Message(name, "372", [connection.nick, ":- " ~ line], true));
connection.sendNumeric!RPL_MOTD("- " ~ line);
}
connection.send(Message(name, "376", [
connection.nick, "End of MOTD command"
], true));
connection.sendNumeric!RPL_ENDOFMOTD();
}
void sendLusers(Connection connection)
@ -369,79 +366,58 @@ class Server
//TODO: If RFC-strictness is off, use '1 server' instead of '1 servers' if the network (or the part of the network of the query) has only one server
//TODO: Support services and multiple servers
connection.send(Message(name, "251", [
connection.nick,
"There are " ~ connections.filter!(c => c.registered)
.count
.to!string ~ " users and 0 services on 1 servers"
], true));
connection.sendNumeric!RPL_LUSERCLIENT(
"There are " ~ connections.filter!(c => c.registered)
.count
.to!string ~ " users and 0 services on 1 servers");
if (connections.any!(c => c.isOperator))
{
connection.send(Message(name, "252", [
connection.nick,
connections.count!(c => c.isOperator)
.to!string, "operator(s) online"
], true));
connection.sendNumeric!RPL_LUSEROP(connections.count!(c => c.isOperator)
.to!string);
}
if (connections.any!(c => !c.registered))
{
connection.send(Message(name, "253", [
connection.nick,
connections.count!(c => !c.registered)
.to!string, "unknown connection(s)"
], true));
connection.sendNumeric!RPL_LUSERUNKNOWN(connections.count!(c => !c.registered)
.to!string);
}
if (channels.length > 0)
{
connection.send(Message(name, "254", [
connection.nick, channels.length.to!string,
"channels formed"
], true));
connection.sendNumeric!RPL_LUSERCHANNELS(channels.length.to!string);
}
connection.send(Message(name, "255", [
connection.nick,
"I have " ~ connections.length.to!string ~ " clients and 1 servers"
], true));
connection.sendNumeric!RPL_LUSERME(
"I have " ~ connections.length.to!string ~ " clients and 1 servers");
}
void ison(Connection connection, string[] nicks)
{
auto reply = nicks.filter!(n => canFindConnectionByNick(n)).join(' ');
connection.send(Message(name, "303", [connection.nick, reply], true));
connection.sendNumeric!RPL_ISON(reply);
}
void whois(Connection connection, string mask)
{
auto user = findConnectionByNick(mask)[0];
connection.send(Message(name, "311", [
connection.nick, user.nick, user.user, user.hostname, "*",
user.hostname
], true));
connection.sendNumeric!RPL_WHOISUSER(user.nick, user.user,
user.hostname, "*", user.realname);
//TODO: Send information about the user's actual server (which is not necessarily this one)
connection.send(Message(name, "312", [
connection.nick, user.nick, name, info
], true));
connection.sendNumeric!RPL_WHOISSERVER(user.nick, name, info);
if (user.isOperator)
{
connection.send(Message(name, "313", [
connection.nick, user.nick, "is an IRC operator"
], true));
connection.sendNumeric!RPL_WHOISOPERATOR(user.nick);
}
auto idleSeconds = (Clock.currTime - user.lastMessageTime).total!"seconds";
connection.send(Message(name, "317", [
connection.nick, user.nick, idleSeconds.to!string,
"seconds idle"
], true));
connection.sendNumeric!RPL_WHOISIDLE(user.nick, idleSeconds.to!string);
auto userChannels = user.channels.map!(c => c.nickPrefix(user) ~ c.name).join(' ');
connection.send(Message(name, "319", [
connection.nick, user.nick, userChannels
], true));
connection.sendNumeric!RPL_WHOISCHANNELS(user.nick, userChannels);
}
void kill(Connection killer, string nick, string comment)
@ -482,10 +458,8 @@ class Server
foreach (command, count; _commandUsage)
{
//TODO: Implement remote count
connection.send(Message(name, "212", [
connection.nick, command, count.to!string,
_commandBytes[command].to!string, "0"
], false));
connection.sendNumeric!RPL_STATSCOMMANDS(command,
count.to!string, _commandBytes[command].to!string, "0");
}
}
@ -497,7 +471,7 @@ class Server
auto uptimeString = format!"Server Up %d days %d:%02d:%02d"(uptime.days,
uptime.hours, uptime.minutes, uptime.seconds);
connection.send(Message(name, "242", [connection.nick, uptimeString], true));
connection.sendNumeric!RPL_STATSUPTIME(uptimeString);
}
void setPass(string pass)

View File

@ -27,7 +27,9 @@ version (MaxNickLengthConfigurable) version = NotStrict;
version (NotStrict)
{
version (SupportTLS) {}
version (SupportTLS)
{
}
else
{
static assert(false, "TLS support must be enabled if any non-strict versions are enabled.");