Compare commits

...

55 Commits

Author SHA1 Message Date
Les De Ridder 8e461051cd Add basic PROXY (version 1) debug support 2020-11-16 01:11:23 +01:00
Les De Ridder 5eea8a71f1 Update readme 2020-11-14 23:10:11 +01:00
Les De Ridder 95f3d8fabd Improve connection handling 2020-11-14 22:43:06 +01:00
Les De Ridder c77709a813 Fix nick changing bug 2020-10-20 09:21:15 +02:00
Les De Ridder d62993c800 Refactor numeric replies 2020-10-17 00:34:06 +02:00
Les De Ridder be963dcf29 Format code with dfmt 2020-10-16 01:48:18 +02:00
Les De Ridder f70b9196c9 Send 002, 003, and 004 messages on user registration 2020-10-16 01:42:26 +02:00
Les De Ridder 2aaa367c10 Refactor version info generation 2020-10-16 01:42:26 +02:00
Les De Ridder 5bb99c2a8c Fix some ban/exemption/invite semantics 2020-10-16 01:42:26 +02:00
Les De Ridder 3f5f1f9ada Allow querying mask list modes with consistent syntax (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder cd7613ed70 Handle plain nicks on +b/+e/+I and send error on invalid mask (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder b5616c4a0b Send ERR_NOSUCHCHANNEL on MODE for non-existent channel (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder a3f306b9ba Send error on invalid MODE operation (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder f6eece74bf Send a different message on viewing other users' modes (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder 49f8cfa3ce Send ERR_NOSUCHNICK on MODE with invalid target (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder a59fdc67ce Send ERR_NOSUCHCHANNEL on TOPIC for non-existent channel (non-strict) 2020-10-16 01:42:26 +02:00
Les De Ridder 21368785d6 Handle incorrect server password 2020-10-14 06:35:01 +02:00
Les De Ridder 3c06c1b738 Ignore command case (non-strict) 2020-10-14 06:01:31 +02:00
Les De Ridder 66175b4168 Clear invite holders when setting +i (non-strict) 2020-10-14 06:01:31 +02:00
Les De Ridder 7a8b28f0f4 Fix versions and add 'modern' configuration 2020-10-14 05:35:16 +02:00
Les De Ridder 8072bfd0f3 Update dependencies 2020-10-14 05:13:36 +02:00
Les De Ridder 9c3f902bfd Prepare for compile-time versions 2020-10-14 05:09:41 +02:00
Les De Ridder a9a0f5564a Update license year 2020-02-12 15:50:21 +01:00
Les De Ridder 2561145d05 Implement JOIN 0 (equivalent to PARTing all channels) 2020-02-12 15:23:31 +01:00
Les De Ridder 9543e78c5c Implement channel member limit and channel key (password) 2020-02-12 15:18:19 +01:00
Les De Ridder 786a3f17e8 Ignore JOIN if user is already on channel 2020-02-12 14:06:10 +01:00
Les De Ridder f80070aa92 Format code (with dfmt --align_switch_statements false) 2020-02-12 13:59:41 +01:00
Les De Ridder 3b93ecc60e Fix IPv6 address 'hostname' generation 2020-02-12 13:55:27 +01:00
Les De Ridder 85d7b02c0d Move output binary to separate directory 2020-02-11 15:26:40 +01:00
Les De Ridder aede39a00c Update dependencies and remove 002/003/004 2020-02-11 15:25:14 +01:00
Les De Ridder d376977326 Convert tabs to spaces 2020-02-11 15:01:08 +01:00
Les De Ridder b7868a87c5 Implement basic config loading and PASS message 2017-12-29 14:38:13 +01:00
Les De Ridder 4407a7419b
Allow channel operators to invite users to a channel with +i 2017-05-24 00:39:55 +02:00
Les De Ridder 7ba5268e90
Check if the user is allowed to join a channel 2017-05-24 00:27:54 +02:00
Les De Ridder 3be4710c32
Fix a bug where we were not sending a channel mode change 2017-05-24 00:12:17 +02:00
Les De Ridder fd8a9aafe7
Check if the user is allowed to send to a channel 2017-05-24 00:06:11 +02:00
Les De Ridder f7edfa9e8f
Implement STATS commands usage and server uptime querying 2017-05-19 01:19:23 +02:00
Les De Ridder 18271e9f49
Clarify RFC compliant mode and other modes 2017-05-18 20:42:10 +02:00
Les De Ridder ad4b2a22ea
Reverse order of RPL_INVITING and update readme
See https://www.rfc-editor.org/errata/eid2821 for the erratum that
caused this change.
2017-05-18 20:26:29 +02:00
Les De Ridder b8484c60d4
Fix miscellaneous bugs and TODOs 2017-05-14 07:06:15 +02:00
Les De Ridder 6a6006c2c5
Implement channel key and user limit management 2017-05-14 05:46:06 +02:00
Les De Ridder f50f602eea
Implement channel ban, exception, and invite list management 2017-05-14 04:59:11 +02:00
Les De Ridder 7df8c916b7
Rename Connection.mask to Connection.prefix 2017-05-14 02:21:40 +02:00
Les De Ridder e507a38e0d
Partially implement channel mode messasge 2017-05-09 06:55:53 +02:00
Les De Ridder d4aaea4f99
Implement user mode message 2017-05-04 07:32:44 +02:00
Les De Ridder 4fa71ee798
Implement KICK 2017-04-30 22:41:48 +02:00
Les De Ridder 3c43cfa64b
Refactor channel management and give +o on creation 2017-04-30 21:05:41 +02:00
Les De Ridder 0bd65fd449
Fix canFindChannelByName 2017-04-30 21:03:18 +02:00
Les De Ridder b4da3ef8fe
Implement KILL 2017-04-24 06:57:21 +02:00
Les De Ridder 16044982a1
Refactor Connection.handle 2017-04-24 06:30:47 +02:00
Les De Ridder 31c662dcbd
Fix connection search for INVITE 2017-04-24 06:21:18 +02:00
Les De Ridder 0ac5cc07ba
Implement WHOIS 2017-04-24 06:20:18 +02:00
Les De Ridder 132c0229a7
Update dependencies 2017-04-24 05:46:10 +02:00
Les De Ridder f60b3cc988
Use case-insensitive comparisons for nicknames and channel names 2017-04-13 22:20:49 +02:00
Les De Ridder e4fdf0ac91
Implement ISON 2017-04-13 01:46:10 +02:00
15 changed files with 2567 additions and 1194 deletions

5
.gitignore vendored
View File

@ -4,6 +4,7 @@ __dummy.html
*.o
*.obj
__test__*__
/salty-ircd
source/ircd/packageVersion.d
out/
source/ircd/versionInfo.d
motd
config.sdl

View File

@ -1,7 +1,7 @@
University of Illinois/NCSA
Open Source License
Copyright (c) 2017, Les De Ridder
Copyright (c) 2017-2020, Les De Ridder
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of

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, 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).
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.
Any additional features breaking RFC compliance are available through compile-time options.
### Security
* 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.
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).

10
config.template.sdl Normal file
View File

@ -0,0 +1,10 @@
listen {
type "plaintext"
address "::"
port 6667
}
# No password set
pass "";
# vim:ft=

15
dub.sdl
View File

@ -4,7 +4,16 @@ authors "Les De Ridder"
copyright "Copyright © 2017, Les De Ridder"
license "NCSA"
targetType "executable"
dependency "vibe-d:core" version="~>0.7.30"
dependency "gen-package-version" version="~>1.0.5"
preGenerateCommands "dub run gen-package-version -- ircd --src=source/"
dependency "vibe-core" version="~>1.8.1"
dependency "vibe-d:stream" version="~>0.9.0-alpha.1"
dependency "sdlang-d" version="~>0.10.5"
preBuildCommands "./generate-version-info.fish"
versions "VibeDefaultMain"
targetPath "out"
configuration "compliant" {
}
configuration "modern" {
versions "Modern"
}

View File

@ -1,15 +1,14 @@
{
"fileVersion": 1,
"versions": {
"diet-ng": "1.2.0",
"eventcore": "0.8.8",
"gen-package-version": "1.0.5",
"libasync": "0.7.9",
"libevent": "2.0.2+2.0.16",
"memutils": "0.4.9",
"openssl": "1.1.5+1.0.1g",
"scriptlike": "0.9.7",
"taggedalgebraic": "0.10.5",
"vibe-d": "0.7.30"
"eventcore": "0.8.50",
"libasync": "0.8.6",
"memutils": "1.0.4",
"sdlang-d": "0.10.6",
"stdx-allocator": "2.77.5",
"taggedalgebraic": "0.11.18",
"unit-threaded": "0.7.55",
"vibe-core": "1.8.1",
"vibe-d": "0.9.2"
}
}

6
generate-version-info.fish Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/fish
set gitVersion (git describe)
set buildDate (date --iso-8601=seconds)
echo -e "/* This file is generated on build! */\n\nmodule ircd.versionInfo;\n\nenum gitVersion = \"$gitVersion\";\nenum buildDate = \"$buildDate\";" > source/ircd/versionInfo.d

View File

@ -1,10 +1,72 @@
module ircd.app;
import std.algorithm;
import std.traits;
import std.string;
import sdlang;
import ircd.server;
shared static this()
static T tagValueOrNull(T)(Tag tag, string childName)
{
auto server = new Server();
server.listen();
if (childName !in tag.tags)
{
return null;
}
else
{
return tagValue!T(tag, childName);
}
}
static T tagValue(T)(Tag tag, string childName)
{
static if (isArray!T && !isSomeString!T)
{
template U(T : T[])
{
alias U = T;
}
T array = [];
foreach (value; tag.tags[childName][0].values)
{
array ~= value.get!(U!T);
}
return array;
}
else static if (isIntegral!T && !is(T == int))
{
return cast(T) tagValue!int(tag, childName);
}
else
{
return tag.tags[childName][0].values[0].get!T;
}
}
shared static this()
{
auto server = new Server();
auto config = parseFile("config.sdl");
auto pass = config.tagValue!string("pass");
server.setPass(pass.empty ? null : pass);
foreach (listenBlock; config.tags.filter!(t => t.getFullName.toString == "listen"))
{
assert(listenBlock.tagValue!string("type") == "plaintext");
auto addresses = listenBlock.tagValue!(string[])("address");
auto port = listenBlock.tagValue!ushort("port");
foreach (address; addresses)
{
server.listen(port, address);
}
}
}

View File

@ -2,99 +2,385 @@ module ircd.channel;
import std.algorithm;
import std.string;
import std.typecons : Nullable;
import ircd.connection;
import ircd.server;
import ircd.message;
import ircd.helpers;
import ircd.numerics;
//TODO: Make this a struct?
class Channel
{
string name;
string name;
string topic = "";
Connection[] members;
Connection owner;
Connection[] members;
char[] modes;
char[][Connection] memberModes;
string[][char] maskLists;
string topic = "";
string key;
Nullable!uint memberLimit;
Connection[] inviteHolders;
char[] modes;
private Server _server;
private Server _server;
this(string name, Server server)
{
this.name = name;
this._server = server;
this.maskLists = ['b': [], 'e': [], 'I': []];
}
this(string name, Connection owner, Server server)
{
this.name = name;
this.owner = owner;
this.members = [owner];
this._server = server;
}
void join(Connection connection)
{
members ~= connection;
void sendNames(Connection connection, bool sendRplEndOfNames = true)
{
string channelType;
if (members.length == 1)
{
memberModes[connection] ~= 'o';
}
else
{
memberModes[connection] = [];
}
if(modes.canFind('s'))
{
channelType = "@";
}
else if(modes.canFind('p'))
{
channelType = "*";
}
else
{
channelType = "=";
}
if (inviteHolders.canFind(connection))
{
inviteHolders = inviteHolders.remove!(c => c == connection);
}
}
auto onChannel = members.canFind(connection);
void part(Connection connection, string partMessage)
{
foreach (member; members)
{
if (partMessage !is null)
{
member.send(Message(connection.prefix, "PART", [
name, partMessage
], true));
}
else
{
member.send(Message(connection.prefix, "PART", [name]));
}
}
connection.send(Message(_server.name, "353", [connection.nick, channelType, name, members.filter!(m => onChannel || !m.modes.canFind('i')).map!(m => m.nick).join(' ')], true));
members = members.remove!(m => m == connection);
memberModes.remove(connection);
}
if(sendRplEndOfNames)
{
connection.sendRplEndOfNames(name);
}
}
void invite(Connection connection)
{
inviteHolders ~= connection;
}
void sendPrivMsg(Connection sender, string text)
{
foreach(member; members.filter!(m => m.nick != sender.nick))
{
member.send(Message(sender.mask, "PRIVMSG", [name, text], true));
}
}
void sendNames(Connection connection, bool sendRplEndOfNames = true)
{
string channelType;
void sendNotice(Connection sender, string text)
{
foreach(member; members.filter!(m => m.nick != sender.nick))
{
member.send(Message(sender.mask, "NOTICE", [name, text], true));
}
}
if (modes.canFind('s'))
{
channelType = "@";
}
else if (modes.canFind('p'))
{
channelType = "*";
}
else
{
channelType = "=";
}
void sendTopic(Connection connection)
{
if(topic.empty)
{
connection.send(Message(_server.name, "331", [connection.nick, name, "No topic is set"]));
}
else
{
connection.send(Message(_server.name, "332", [connection.nick, name, topic], true));
}
}
auto onChannel = members.canFind(connection);
void setTopic(Connection connection, string newTopic)
{
topic = newTopic;
connection.sendNumeric!RPL_NAMREPLY(channelType, name,
members.filter!(m => onChannel || !m.modes.canFind('i'))
.map!(m => prefixedNick(m))
.join(' '));
foreach(member; members)
{
member.send(Message(connection.mask, "TOPIC", [name, newTopic], true));
}
}
if (sendRplEndOfNames)
{
connection.sendNumeric!RPL_ENDOFNAMES(name);
}
}
bool visibleTo(Connection connection)
{
return members.canFind(connection) || !modes.canFind('s') && !modes.canFind('p');
}
void sendPrivMsg(Connection sender, string text)
{
foreach (member; members.filter!(m => m.nick != sender.nick))
{
member.send(Message(sender.prefix, "PRIVMSG", [name, text], true));
}
}
void sendNotice(Connection sender, string text)
{
foreach (member; members.filter!(m => m.nick != sender.nick))
{
member.send(Message(sender.prefix, "NOTICE", [name, text], true));
}
}
void sendTopic(Connection connection)
{
if (topic.empty)
connection.sendNumeric!RPL_NOTOPIC(name);
else
connection.sendNumeric!RPL_TOPIC(name, topic);
}
void setTopic(Connection connection, string newTopic)
{
topic = newTopic;
foreach (member; members)
{
member.send(Message(connection.prefix, "TOPIC", [name, newTopic], true));
}
}
void kick(Connection kicker, Connection user, string comment)
{
foreach (member; members)
{
member.send(Message(kicker.prefix, "KICK", [
name, user.nick, comment
], true));
}
members = members.remove!(m => m == user);
memberModes.remove(user);
}
void sendModes(Connection user)
{
auto specialModes = "";
string[] specialModeParameters;
if (members.canFind(user) && key !is null)
{
specialModes ~= "k";
specialModeParameters ~= key;
}
if (members.canFind(user) && !memberLimit.isNull)
{
import std.conv : to;
specialModes ~= "l";
specialModeParameters ~= memberLimit.to!string;
}
user.sendNumeric!RPL_CHANNELMODEIS([name,
"+" ~ modes.idup ~ specialModes] ~ specialModeParameters);
}
bool setMemberMode(Connection target, char mode)
{
if (memberModes[target].canFind(mode))
{
return false;
}
memberModes[target] ~= mode;
return true;
}
bool unsetMemberMode(Connection target, char mode)
{
if (!memberModes[target].canFind(mode))
{
return false;
}
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
import std.utf : byCodeUnit;
import std.range : array;
memberModes[target] = memberModes[target].byCodeUnit.remove!(m => m == mode).array;
return true;
}
bool setMode(char mode)
{
if (modes.canFind(mode))
{
return false;
}
modes ~= mode;
//NOTE: The RFCs don't specify that the invite list should be cleared on +i
version (BasicFixes)
{
if (mode == 'i')
{
inviteHolders = [];
}
}
return true;
}
bool unsetMode(char mode)
{
if (!modes.canFind(mode))
{
return false;
}
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
import std.utf : byCodeUnit;
import std.range : array;
modes = modes.byCodeUnit.remove!(m => m == mode).array;
return true;
}
bool addMaskListEntry(string mask, char mode)
{
if (maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
{
return false;
}
maskLists[mode] ~= mask;
return true;
}
bool removeMaskListEntry(string mask, char mode)
{
if (!maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
{
return false;
}
maskLists[mode] = maskLists[mode].remove!(m => m.toIRCLower == mask.toIRCLower);
return true;
}
void sendBanList(Connection connection)
{
foreach (entry; maskLists['b'])
{
connection.sendNumeric!RPL_BANLIST(name, entry);
}
connection.sendNumeric!RPL_ENDOFBANLIST(name);
}
void sendExceptList(Connection connection)
{
foreach (entry; maskLists['e'])
{
connection.sendNumeric!RPL_EXCEPTLIST(name, entry);
}
connection.sendNumeric!RPL_ENDOFEXCEPTLIST(name);
}
void sendInviteList(Connection connection)
{
foreach (entry; maskLists['I'])
{
connection.sendNumeric!RPL_INVITELIST(name, entry);
}
connection.sendNumeric!RPL_ENDOFINVITELIST(name);
}
bool setKey(string key)
{
this.key = key;
return true;
}
bool unsetKey(string key)
{
if (this.key != key)
{
return false;
}
this.key = null;
return true;
}
void setMemberLimit(uint memberLimit)
{
this.memberLimit = memberLimit;
}
bool unsetMemberLimit()
{
if (memberLimit.isNull)
{
return false;
}
memberLimit.nullify();
return true;
}
string nickPrefix(Connection member)
{
if (!members.canFind(member))
return null;
if (memberModes[member].canFind('o'))
{
return "@";
}
else if (memberModes[member].canFind('v'))
{
return "+";
}
return "";
}
string prefixedNick(Connection member)
{
return nickPrefix(member) ~ member.nick;
}
bool visibleTo(Connection connection)
{
return members.canFind(connection) || !modes.canFind('s') && !modes.canFind('p');
}
bool canReceiveMessagesFromUser(Connection connection)
{
if (modes.canFind('n') && !members.canFind(connection))
{
return false;
}
else if (modes.canFind('m') && nickPrefix(connection).empty)
{
return false;
}
else if (maskLists['b'].any!(m => connection.matchesMask(m))
&& !maskLists['e'].any!(m => connection.matchesMask(m))
&& nickPrefix(connection).length == 0)
{
return false;
}
return true;
}
bool hasMember(Connection connection)
{
return members.canFind(connection);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,71 @@
module ircd.helpers;
import std.range;
import std.range : array, empty, front, popFront, save;
import std.algorithm : map;
//Based on std.path.globMatch (https://github.com/dlang/phobos/blob/v2.073.2/std/path.d#L3164)
//License: Boost License 1.0 (http://www.boost.org/LICENSE_1_0.txt)
//Copyright (c) Lars T. Kyllingstad, Walter Bright
@safe pure
bool wildcardMatch(string input, string pattern)
@safe pure bool wildcardMatch(string input, string pattern)
{
foreach (ref pi; 0 .. pattern.length)
{
const pc = pattern[pi];
switch (pc)
{
case '*':
if (pi + 1 == pattern.length)
{
return true;
}
for (; !input.empty; input.popFront())
{
auto p = input.save;
if (wildcardMatch(p, pattern[pi + 1 .. pattern.length]))
{
return true;
}
}
return false;
case '?':
if (input.empty)
{
return false;
}
input.popFront();
break;
default:
if (input.empty || pc != input.front)
{
return false;
}
input.popFront();
break;
}
}
return input.empty;
foreach (ref pi; 0 .. pattern.length)
{
const pc = pattern[pi];
switch (pc)
{
case '*':
if (pi + 1 == pattern.length)
{
return true;
}
for (; !input.empty; input.popFront())
{
auto p = input.save;
if (wildcardMatch(p, pattern[pi + 1 .. pattern.length]))
{
return true;
}
}
return false;
case '?':
if (input.empty)
{
return false;
}
input.popFront();
break;
default:
if (input.empty || pc != input.front)
{
return false;
}
input.popFront();
break;
}
}
return input.empty;
}
@safe pure dchar toIRCLower(dchar input)
{
import std.uni : toLower;
switch (input)
{
case '[':
return '{';
case ']':
return '}';
case '\\':
return '|';
default:
return input.toLower;
}
}
@safe pure string toIRCLower(string input)
{
import std.utf : byChar;
return input.map!toIRCLower.byChar.array.idup;
}

View File

@ -6,84 +6,86 @@ import std.array;
import std.algorithm;
import std.conv;
//TODO: Make this a class
struct Message
{
string prefix;
string command;
string[] parameters;
bool prefixedParameter;
string prefix;
string command;
string[] parameters;
bool prefixedParameter;
static Message fromString(string line)
{
string prefix = null;
if(line.startsWith(':'))
{
line = line[1 .. $];
prefix = line[0 .. line.indexOf(' ')];
line = line[prefix.length + 1 .. $];
}
//NOTE: The RFCs don't state what this is exactly, but common implementations use the byte count of the message parameters
ulong bytes;
//stop early when no space character can be found (message without parameters)
if(!line.canFind(' '))
{
return Message(prefix, line, [], false);
}
static Message fromString(string line)
{
string prefix = null;
if (line.startsWith(':'))
{
line = line[1 .. $];
prefix = line[0 .. line.indexOf(' ')];
line = line[prefix.length + 1 .. $];
}
auto command = line[0 .. line.indexOf(' ')];
line = line[command.length + 1 .. $];
string[] params = [];
bool prefixedParam;
while(true)
{
if(line.startsWith(':'))
{
params ~= line[1 .. $];
prefixedParam = true;
break;
}
else if(line.canFind(' '))
{
auto param = line[0 .. line.indexOf(' ')];
line = line[param.length + 1 .. $];
params ~= param;
}
else
{
params ~= line;
break;
}
}
//stop early when no space character can be found (message without parameters)
if (!line.canFind(' '))
{
return Message(prefix, line, [], false);
}
return Message(prefix, command, params, prefixedParam);
}
auto command = line[0 .. line.indexOf(' ')];
line = line[command.length + 1 .. $];
auto bytes = line.length;
string[] params = [];
bool prefixedParam;
while (true)
{
if (line.startsWith(':'))
{
params ~= line[1 .. $];
prefixedParam = true;
break;
}
else if (line.canFind(' '))
{
auto param = line[0 .. line.indexOf(' ')];
line = line[param.length + 1 .. $];
params ~= param;
}
else
{
params ~= line;
break;
}
}
string toString()
{
auto message = "";
if(prefix != null)
{
message = ":" ~ prefix ~ " ";
}
return Message(prefix, command, params, prefixedParam, bytes);
}
if(parameters.length == 0)
{
return message ~ command;
}
string toString()
{
auto message = "";
if (prefix != null)
{
message = ":" ~ prefix ~ " ";
}
message ~= command ~ " ";
if(parameters.length > 1)
{
message ~= parameters[0 .. $-1].join(' ') ~ " ";
}
if (parameters.length == 0)
{
return message ~ command;
}
if(parameters[$-1].canFind(' ') || prefixedParameter)
{
message ~= ":";
}
message ~= parameters[$-1];
message ~= command ~ " ";
if (parameters.length > 1)
{
message ~= parameters[0 .. $ - 1].join(' ') ~ " ";
}
return message;
}
if (parameters[$ - 1].canFind(' ') || prefixedParameter)
{
message ~= ":";
}
message ~= parameters[$ - 1];
return message;
}
}

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

@ -10,340 +10,492 @@ import std.datetime;
import std.string;
import vibe.core.core;
import vibe.core.net;
import ircd.packageVersion;
import ircd.versionInfo;
import ircd.message;
import ircd.connection;
import ircd.channel;
import ircd.helpers;
import ircd.numerics;
//TODO: Make this a struct?
class Server
{
Connection[] connections;
Connection[] connections;
enum creationDate = packageTimestampISO.until('T').text; //TODO: Also show time when RFC-strictness is off
enum versionString = "salty-ircd-" ~ packageVersion;
enum versionString = "salty-ircd-" ~ gitVersion;
string name;
string name;
enum string info = "A salty-ircd server"; //TODO: Make server info configurable
string motd;
string motd;
Channel[] channels;
Channel[] channels;
this()
{
name = Socket.hostName;
private uint[string] _commandUsage;
private ulong[string] _commandBytes;
readMotd();
private string _pass = null;
runTask(&pingLoop);
}
private SysTime _startTime;
private void readMotd()
{
import std.file : exists, readText;
if(exists("motd"))
{
motd = readText("motd");
}
}
this()
{
name = Socket.hostName;
private void pingLoop()
{
while(true)
{
foreach(connection; connections)
{
connection.send(Message(null, "PING", [name], true));
}
sleep(30.seconds);
}
}
readMotd();
private void acceptConnection(TCPConnection tcpConnection)
{
auto connection = new Connection(tcpConnection, this);
connections ~= connection;
connection.handle();
connections = connections.filter!(c => c != connection).array;
}
_startTime = Clock.currTime;
static bool isValidChannelName(string name)
{
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
}
runTask(&pingLoop);
}
static bool isValidNick(string name)
{
import std.ascii : digits, letters;
private void readMotd()
{
import std.file : exists, readText;
if(name.length > 9)
{
return false;
}
foreach(i, c; name)
{
auto allowed = letters ~ "[]\\`_^{|}";
if(i > 0)
{
allowed ~= digits ~ "-";
}
if (exists("motd"))
{
motd = readText("motd");
}
}
if (!allowed.canFind(c))
{
return false;
}
}
return true;
}
private void pingLoop()
{
while (true)
{
foreach (connection; connections)
{
connection.send(Message(null, "PING", [name], true));
}
sleep(30.seconds);
}
}
bool isNickAvailable(string nick)
{
return !connections.canFind!(c => c.nick == nick);
}
private void acceptConnection(TCPConnection tcpConnection)
{
auto connection = new Connection(tcpConnection, this);
connections ~= connection;
connection.handle();
connections = connections.filter!(c => c != connection).array;
}
void join(Connection connection, string channelName)
{
auto channelRange = channels.find!(c => c.name == channelName);
Channel channel;
if(channelRange.empty)
{
channel = new Channel(channelName, connection, this);
channels ~= channel;
}
else
{
channel = channelRange[0];
channel.members ~= connection;
}
static bool isValidChannelName(string name)
{
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
}
foreach(member; channel.members)
{
member.send(Message(connection.mask, "JOIN", [channelName]));
}
static bool isValidNick(string name)
{
import std.ascii : digits, letters;
channel.sendNames(connection);
if (name.length > 9)
{
return false;
}
foreach (i, c; name)
{
auto allowed = letters ~ "[]\\`_^{|}";
if (i > 0)
{
allowed ~= digits ~ "-";
}
if(!channel.topic.empty)
{
channel.sendTopic(connection);
}
}
if (!allowed.canFind(c))
{
return false;
}
}
return true;
}
void part(Connection connection, string channelName, string partMessage)
{
auto channel = connection.channels.array.find!(c => c.name == channelName)[0];
static bool isValidUserMask(string mask)
{
import std.regex : ctRegex, matchFirst;
foreach(member; channel.members)
{
if(partMessage !is null)
{
member.send(Message(connection.mask, "PART", [channelName, partMessage], true));
}
else
{
member.send(Message(connection.mask, "PART", [channelName]));
}
}
auto validMaskRegex = ctRegex!r"^([^!]+)!([^@]+)@(.+)$";
return !mask.matchFirst(validMaskRegex).empty;
}
channel.members = channel.members.remove!(m => m == connection);
Connection[] findConnectionByNick(string nick)
{
return connections.find!(c => c.nick.toIRCLower == nick.toIRCLower);
}
if(channel.members.length == 0)
{
channels = channels.remove!(c => c == channel);
}
}
bool canFindConnectionByNick(string nick)
{
return !findConnectionByNick(nick).empty;
}
void quit(Connection connection, string quitMessage)
{
Connection[] peers;
foreach(channel; connection.channels)
{
peers ~= channel.members;
channel.members = channel.members.remove!(m => m == connection);
if(channel.members.length == 0)
{
channels = channels.remove!(c => c == channel);
}
}
peers = peers.sort().uniq.filter!(c => c != connection).array;
bool isNickAvailable(string nick)
{
return !canFindConnectionByNick(nick);
}
foreach(peer; peers)
{
if(quitMessage !is null)
{
peer.send(Message(connection.mask, "QUIT", [quitMessage], true));
}
else
{
peer.send(Message(connection.mask, "QUIT", [connection.nick], true));
}
}
}
Channel[] findChannelByName(string name)
{
return channels.find!(c => c.name.toIRCLower == name.toIRCLower);
}
void whoChannel(Connection origin, string channelName, bool operatorsOnly)
{
//TODO: Check what RFCs say about secret/private channels
bool canFindChannelByName(string name)
{
return !findChannelByName(name).empty;
}
auto channel = channels.find!(c => c.name == channelName)[0];
foreach(c; channel.members.filter!(c => !operatorsOnly || c.isOperator)
.filter!(c => c.visibleTo(origin)))
{
//TODO: Support hop count
origin.sendWhoReply(channelName, c, 0);
}
}
void join(Connection connection, string channelName)
{
auto channelRange = findChannelByName(channelName);
Channel channel;
if (channelRange.empty)
{
channel = new Channel(channelName, this);
channels ~= channel;
}
else
{
channel = channelRange[0];
}
channel.join(connection);
void whoGlobal(Connection origin, string mask, bool operatorsOnly)
{
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))))
{
//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;
//TODO: Support hop count
origin.sendWhoReply(channelName, c, 0);
}
}
foreach (member; channel.members)
{
member.send(Message(connection.prefix, "JOIN", [channelName]));
}
void privmsgToChannel(Connection sender, string target, string text)
{
auto channel = channels.find!(c => c.name == target)[0];
channel.sendPrivMsg(sender, text);
}
channel.sendNames(connection);
void privmsgToUser(Connection sender, string target, string text)
{
auto user = connections.find!(c => c.nick == target)[0];
user.send(Message(sender.mask, "PRIVMSG", [target, text], true));
}
if (!channel.topic.empty)
{
channel.sendTopic(connection);
}
}
void noticeToChannel(Connection sender, string target, string text)
{
auto channel = channels.find!(c => c.name == target)[0];
channel.sendNotice(sender, text);
}
void part(Connection connection, string channelName, string partMessage)
{
auto channel = connection.channels.array.find!(
c => c.name.toIRCLower == channelName.toIRCLower)[0];
void noticeToUser(Connection sender, string target, string text)
{
auto user = connections.find!(c => c.nick == target)[0];
user.send(Message(sender.mask, "NOTICE", [target, text], true));
}
channel.part(connection, partMessage);
void sendChannelTopic(Connection origin, string channelName)
{
auto channel = channels.find!(c => c.name == channelName)[0];
channel.sendTopic(origin);
}
if (channel.members.empty)
{
channels = channels.remove!(c => c == channel);
}
}
void setChannelTopic(Connection origin, string channelName, string newTopic)
{
auto channel = channels.find!(c => c.name == channelName)[0];
channel.setTopic(origin, newTopic);
}
void quit(Connection connection, string quitMessage)
{
Connection[] peers;
foreach (channel; connection.channels)
{
peers ~= channel.members;
channel.members = channel.members.remove!(m => m == connection);
if (channel.members.empty)
{
channels = channels.remove!(c => c == channel);
}
}
peers = peers.sort().uniq.filter!(c => c != connection).array;
void sendChannelNames(Connection connection, string channelName)
{
auto channel = channels.find!(c => c.name == channelName)[0];
channel.sendNames(connection);
}
foreach (peer; peers)
{
if (quitMessage !is null)
{
peer.send(Message(connection.prefix, "QUIT", [quitMessage], true));
}
else
{
peer.send(Message(connection.prefix, "QUIT", [connection.nick], true));
}
}
}
void sendGlobalNames(Connection connection)
{
foreach(channel; channels.filter!(c => c.visibleTo(connection)))
{
channel.sendNames(connection, false);
}
void whoChannel(Connection origin, string channelName, bool operatorsOnly)
{
//TODO: Check what RFCs say about secret/private channels
auto otherUsers = connections.filter!(c => !c.modes.canFind('i') && c.channels.filter!(ch => !ch.modes.canFind('s') && !ch.modes.canFind('p')).empty);
if(!otherUsers.empty)
{
connection.send(Message(name, "353", [connection.nick, "=", "*", otherUsers.map!(m => m.nick).join(' ')], true));
}
auto channel = findChannelByName(channelName)[0];
foreach (c; channel.members
.filter!(c => !operatorsOnly || c.isOperator)
.filter!(c => c.visibleTo(origin)))
{
//TODO: Support hop count
origin.sendWhoReply(channelName, c, channel.nickPrefix(c), 0);
}
}
connection.sendRplEndOfNames("*");
}
void whoGlobal(Connection origin, string mask, bool operatorsOnly)
{
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))))
{
//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;
auto nickPrefix = c.channels.empty ? "" : c.channels.array[0].nickPrefix(c);
//TODO: Support hop count
origin.sendWhoReply(channelName, c, nickPrefix, 0);
}
}
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.sendRplListEnd();
}
void privmsgToChannel(Connection sender, string target, string text)
{
auto channel = findChannelByName(target)[0];
channel.sendPrivMsg(sender, text);
}
void sendPartialList(Connection connection, string[] channelNames)
{
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.sendRplListEnd();
}
void privmsgToUser(Connection sender, string target, string text)
{
auto user = findConnectionByNick(target)[0];
user.send(Message(sender.prefix, "PRIVMSG", [target, text], true));
}
void sendVersion(Connection connection)
{
connection.send(Message(name, "351", [connection.nick, versionString ~ ".", name, ""], true));
}
void noticeToChannel(Connection sender, string target, string text)
{
auto channel = findChannelByName(target)[0];
channel.sendNotice(sender, text);
}
void sendTime(Connection connection)
{
auto timeString = Clock.currTime.toISOExtString;
connection.send(Message(name, "391", [connection.nick, name, timeString], true));
}
void noticeToUser(Connection sender, string target, string text)
{
auto user = findConnectionByNick(target)[0];
user.send(Message(sender.prefix, "NOTICE", [target, text], true));
}
void invite(Connection inviter, string target, string channelName)
{
auto user = connections.find!(c => c.nick = target)[0];
user.send(Message(inviter.mask, "INVITE", [user.nick, channelName]));
}
void sendChannelTopic(Connection origin, string channelName)
{
auto channel = findChannelByName(channelName)[0];
channel.sendTopic(origin);
}
void sendMotd(Connection connection)
{
connection.send(Message(name, "375", [connection.nick, ":- " ~ name ~ " Message of the day - "], true));
foreach(line; motd.splitLines)
{
//TODO: Implement line wrapping
connection.send(Message(name, "372", [connection.nick, ":- " ~ line], true));
}
connection.send(Message(name, "376", [connection.nick, "End of MOTD command"], true));
}
void setChannelTopic(Connection origin, string channelName, string newTopic)
{
auto channel = findChannelByName(channelName)[0];
channel.setTopic(origin, newTopic);
}
void sendLusers(Connection connection)
{
//TODO: If RFC-strictness is off, use '1 server' instead of '1 servers' of the network (or the part of the network of the query) has only one server
void sendChannelNames(Connection connection, string channelName)
{
auto channel = findChannelByName(channelName)[0];
channel.sendNames(connection);
}
//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));
void sendGlobalNames(Connection connection)
{
foreach (channel; channels.filter!(c => c.visibleTo(connection)))
{
channel.sendNames(connection, false);
}
if(connections.any!(c => c.isOperator))
{
connection.send(Message(name, "252", [connection.nick, connections.count!(c => c.isOperator).to!string, "operator(s) online"], true));
}
auto otherUsers = connections.filter!(c => !c.modes.canFind('i')
&& c.channels.filter!(ch => !ch.modes.canFind('s')
&& !ch.modes.canFind('p')).empty);
if (!otherUsers.empty)
{
connection.sendNumeric!RPL_NAMREPLY("=", "*", otherUsers.map!(m => m.nick).join(' '));
}
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_ENDOFNAMES("*");
}
if(channels.length > 0)
{
connection.send(Message(name, "254", [connection.nick, channels.length.to!string, "channels formed"], true));
}
void sendFullList(Connection connection)
{
foreach (channel; channels.filter!(c => c.visibleTo(connection)))
{
connection.sendNumeric!RPL_LIST(channel.name, channel.members
.filter!(m => m.visibleTo(connection))
.array
.length
.to!string, channel.topic);
}
connection.sendNumeric!RPL_LISTEND();
}
connection.send(Message(name, "255", [connection.nick, "I have " ~ connections.length.to!string ~ " clients and 1 servers"], true));
}
void sendPartialList(Connection connection, string[] channelNames)
{
foreach (channel; channels.filter!(c => channelNames.canFind(c.name)
&& c.visibleTo(connection)))
{
connection.sendNumeric!RPL_LIST(channel.name, channel.members
.filter!(m => m.visibleTo(connection))
.array
.length
.to!string, channel.topic);
}
connection.sendNumeric!RPL_LISTEND();
}
void listen(ushort port = 6667)
{
listenTCP(port, &acceptConnection);
}
void sendVersion(Connection connection)
{
//TODO: Include enabled versions in comments?
connection.sendNumeric!RPL_VERSION(versionString ~ ".", name, ":");
}
void listen(ushort port, string address)
{
listenTCP(port, &acceptConnection, address);
}
void sendTime(Connection connection)
{
auto timeString = Clock.currTime.toISOExtString;
connection.sendNumeric!RPL_TIME(name, timeString);
}
void invite(Connection inviter, string target, string channelName)
{
auto user = findConnectionByNick(target)[0];
auto channel = findChannelByName(channelName)[0];
channel.invite(user);
user.send(Message(inviter.prefix, "INVITE", [user.nick, channelName]));
}
void sendMotd(Connection connection)
{
connection.sendNumeric!RPL_MOTDSTART("- " ~ name ~ " Message of the day - ");
foreach (line; motd.splitLines)
{
//TODO: Implement line wrapping
connection.sendNumeric!RPL_MOTD("- " ~ line);
}
connection.sendNumeric!RPL_ENDOFMOTD();
}
void sendLusers(Connection connection)
{
//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.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.sendNumeric!RPL_LUSEROP(connections.count!(c => c.isOperator)
.to!string);
}
if (connections.any!(c => !c.registered))
{
connection.sendNumeric!RPL_LUSERUNKNOWN(connections.count!(c => !c.registered)
.to!string);
}
if (channels.length > 0)
{
connection.sendNumeric!RPL_LUSERCHANNELS(channels.length.to!string);
}
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.sendNumeric!RPL_ISON(reply);
}
void whois(Connection connection, string mask)
{
auto user = findConnectionByNick(mask)[0];
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.sendNumeric!RPL_WHOISSERVER(user.nick, name, info);
if (user.isOperator)
{
connection.sendNumeric!RPL_WHOISOPERATOR(user.nick);
}
auto idleSeconds = (Clock.currTime - user.lastMessageTime).total!"seconds";
connection.sendNumeric!RPL_WHOISIDLE(user.nick, idleSeconds.to!string);
auto userChannels = user.channels.map!(c => c.nickPrefix(user) ~ c.name).join(' ');
connection.sendNumeric!RPL_WHOISCHANNELS(user.nick, userChannels);
}
void kill(Connection killer, string nick, string comment)
{
auto user = findConnectionByNick(nick)[0];
user.send(Message(killer.prefix, "KILL", [nick, comment], true));
quit(user, "Killed by " ~ killer.nick ~ " (" ~ comment ~ ")");
user.send(Message(null, "ERROR",
["Closing Link: Killed by " ~ killer.nick ~ " (" ~ comment ~ ")"], true));
user.closeConnection();
}
void kick(Connection kicker, string channelName, string nick, string comment)
{
auto channel = findChannelByName(channelName)[0];
auto user = findConnectionByNick(nick)[0];
channel.kick(kicker, user, comment);
}
void updateCommandStatistics(Message message)
{
auto command = message.command.toUpper;
if (command !in _commandUsage)
{
_commandUsage[command] = 0;
_commandBytes[command] = 0;
}
_commandUsage[command]++;
_commandBytes[command] += message.bytes;
}
void sendCommandUsage(Connection connection)
{
foreach (command, count; _commandUsage)
{
//TODO: Implement remote count
connection.sendNumeric!RPL_STATSCOMMANDS(command,
count.to!string, _commandBytes[command].to!string, "0");
}
}
void sendUptime(Connection connection)
{
import std.format : format;
auto uptime = (Clock.currTime - _startTime).split!("days", "hours", "minutes", "seconds");
auto uptimeString = format!"Server Up %d days %d:%02d:%02d"(uptime.days,
uptime.hours, uptime.minutes, uptime.seconds);
connection.sendNumeric!RPL_STATSUPTIME(uptimeString);
}
void setPass(string pass)
{
_pass = pass;
}
bool isPassCorrect(string pass)
{
return pass == _pass;
}
bool hasPass()
{
return _pass != null;
}
void listen(ushort port = 6667)
{
listenTCP(port, &acceptConnection);
}
void listen(ushort port, string address)
{
listenTCP(port, &acceptConnection, address);
}
}

37
source/ircd/versions.d Normal file
View File

@ -0,0 +1,37 @@
module ircd.versions;
/++
Supported versions:
* SupportTLS: compile with TLS support
* BasicFixes: enable basic/sanity RFC fixes
* MaxNickLengthConfigurable: makes max nick length configurable
* Modern: enable all versions
(* NotStrict: enabled when any versions are enabled that disable RFC-strictness, i.e. any of the above)
+/
//TODO: Implement 'SupportTLS'
//TODO: Implement 'MaxNickLengthConfigurable'
version (Modern)
{
version = SupportTLS;
version = BasicFixes;
version = MaxNickLengthConfigurable;
}
version (SupportTLS) version = NotStrict;
version (BasicFixes) version = NotStrict;
version (MaxNickLengthConfigurable) version = NotStrict;
version (NotStrict)
{
version (SupportTLS)
{
}
else
{
static assert(false, "TLS support must be enabled if any non-strict versions are enabled.");
}
}