Compare commits

...

77 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
Les De Ridder 2bfa4a75bf
Implement LUSERS 2017-04-10 05:07:19 +02:00
Les De Ridder 324cee253b
Fix Connection sorting 2017-04-10 03:07:05 +02:00
Les De Ridder dd33015ec7
Merge branch 'valnick' of albino/salty-ircd into master 2017-04-08 00:18:00 +02:00
Al Beano 9c8caae67d
Validate nicknames 2017-04-07 22:58:03 +01:00
Les De Ridder d78ca949af
Implement MOTD 2017-04-07 08:08:24 +02:00
Les De Ridder ffa830cf04
Implement TIME 2017-03-22 17:33:56 +01:00
Les De Ridder cfc1345682
Implement VERSION 2017-03-22 17:16:43 +01:00
Les De Ridder 5c67c483ab
Fix welcome message trigger 2017-03-22 17:00:24 +01:00
Les De Ridder db4300eb76
Implement INVITE 2017-03-22 16:58:39 +01:00
Les De Ridder 1861abbb5d
Implement LIST 2017-03-22 16:20:31 +01:00
Les De Ridder d77df55265
Correct typo 2017-03-21 02:55:34 +01:00
Les De Ridder 0fa5f06bd0
Implement NAMES 2017-03-21 02:24:33 +01:00
Les De Ridder 8a94d845e8
Implement TOPIC 2017-03-21 01:28:58 +01:00
Les De Ridder 4f8aedddc9
Implement NOTICE 2017-03-20 05:35:26 +01:00
Les De Ridder 4df3b43138
Implement AWAY 2017-03-20 00:34:26 +01:00
Les De Ridder b1dfa5435f
Update readme 2017-03-19 22:46:36 +01:00
Les De Ridder 1e89db7d0f
Implement WHO query 2017-03-19 22:43:52 +01:00
Les De Ridder 5e2839eb39
Add readme 2017-03-17 21:23:36 +01:00
Les De Ridder 42fa3a0c8c
Keep track of user registration status 2017-03-17 15:48:59 +01:00
Les De Ridder 88c8a99bb7
Stop processing NICK when no parameters are given 2017-03-17 14:50:02 +01:00
Les De Ridder c198279238
Improve USER command handling 2017-03-17 04:03:29 +01:00
Les De Ridder 10bd22734d
Check nick availability 2017-03-17 02:17:23 +01:00
15 changed files with 2630 additions and 474 deletions

6
.gitignore vendored
View File

@ -4,5 +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

42
README.md Normal file
View File

@ -0,0 +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 (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 are available through compile-time options.
### 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
[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,47 +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
{
private string _name;
string name;
string topic = "";
Connection[] members;
Connection owner;
Connection[] members;
char[] modes;
char[][Connection] memberModes;
string[][char] maskLists;
private Server _server;
string key;
Nullable!uint memberLimit;
Connection[] inviteHolders;
this(string name, Connection owner, Server server)
{
this._name = name;
this.owner = owner;
this.members = [owner];
this._server = server;
}
private Server _server;
@property
string name()
{
return _name;
}
this(string name, Server server)
{
this.name = name;
this._server = server;
this.maskLists = ['b': [], 'e': [], 'I': []];
}
void sendNames(Connection connection)
{
enum channelType = "="; //TODO: Support secret and private channels
void join(Connection connection)
{
members ~= connection;
connection.send(Message(_server.name, "353", [connection.nick, channelType, name, members.map!(m => m.nick).join(' ')], true));
connection.send(Message(_server.name, "366", [connection.nick, name, "End of NAMES list"], true));
}
if (members.length == 1)
{
memberModes[connection] ~= 'o';
}
else
{
memberModes[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));
}
}
if (inviteHolders.canFind(connection))
{
inviteHolders = inviteHolders.remove!(c => c == 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]));
}
}
members = members.remove!(m => m == connection);
memberModes.remove(connection);
}
void invite(Connection connection)
{
inviteHolders ~= connection;
}
void sendNames(Connection connection, bool sendRplEndOfNames = true)
{
string channelType;
if (modes.canFind('s'))
{
channelType = "@";
}
else if (modes.canFind('p'))
{
channelType = "*";
}
else
{
channelType = "=";
}
auto onChannel = members.canFind(connection);
connection.sendNumeric!RPL_NAMREPLY(channelType, name,
members.filter!(m => onChannel || !m.modes.canFind('i'))
.map!(m => prefixedNick(m))
.join(' '));
if (sendRplEndOfNames)
{
connection.sendNumeric!RPL_ENDOFNAMES(name);
}
}
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

71
source/ircd/helpers.d Normal file
View File

@ -0,0 +1,71 @@
module ircd.helpers;
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)
{
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

@ -6,157 +6,496 @@ import std.range;
import std.conv;
import std.socket;
import core.time;
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
Channel[] channels;
string motd;
this()
{
name = Socket.hostName;
Channel[] channels;
runTask(&pingLoop);
}
private uint[string] _commandUsage;
private ulong[string] _commandBytes;
private void pingLoop()
{
while(true)
{
foreach(connection; connections)
{
connection.send(Message(null, "PING", [name], true));
}
sleep(30.seconds);
}
}
private string _pass = null;
private void acceptConnection(TCPConnection tcpConnection)
{
auto connection = new Connection(tcpConnection, this);
connections ~= connection;
connection.handle();
connections = connections.filter!(c => c != connection).array;
}
private SysTime _startTime;
static bool isValidChannelName(string name)
{
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
}
this()
{
name = Socket.hostName;
static bool isValidNick(string name)
{
//TODO: Use the real rules
return !name.startsWith('#') && !name.startsWith('&') && name.length <= 9;
}
readMotd();
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;
}
_startTime = Clock.currTime;
foreach(member; channel.members)
{
member.send(Message(connection.mask, "JOIN", [channelName]));
}
runTask(&pingLoop);
}
channel.sendNames(connection);
}
private void readMotd()
{
import std.file : exists, readText;
void part(Connection connection, string channelName, string partMessage)
{
auto channel = connection.channels.array.find!(c => c.name == channelName)[0];
if (exists("motd"))
{
motd = readText("motd");
}
}
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]));
}
}
private void pingLoop()
{
while (true)
{
foreach (connection; connections)
{
connection.send(Message(null, "PING", [name], true));
}
sleep(30.seconds);
}
}
channel.members = channel.members.remove!(m => m == connection);
private void acceptConnection(TCPConnection tcpConnection)
{
auto connection = new Connection(tcpConnection, this);
connections ~= connection;
connection.handle();
connections = connections.filter!(c => c != connection).array;
}
if(channel.members.length == 0)
{
channels = channels.remove!(c => c == channel);
}
}
static bool isValidChannelName(string name)
{
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
}
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;
static bool isValidNick(string name)
{
import std.ascii : digits, letters;
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));
}
}
}
if (name.length > 9)
{
return false;
}
foreach (i, c; name)
{
auto allowed = letters ~ "[]\\`_^{|}";
if (i > 0)
{
allowed ~= digits ~ "-";
}
void sendToChannel(Connection sender, string target, string text)
{
auto channel = channels.find!(c => c.name == target)[0];
channel.sendPrivMsg(sender, text);
}
if (!allowed.canFind(c))
{
return false;
}
}
return true;
}
void sendToUser(Connection sender, string target, string text)
{
auto user = connections.find!(c => c.nick == target)[0];
user.send(Message(sender.mask, "PRIVMSG", [target, text], true));
}
static bool isValidUserMask(string mask)
{
import std.regex : ctRegex, matchFirst;
void listen(ushort port = 6667)
{
listenTCP(port, &acceptConnection);
}
auto validMaskRegex = ctRegex!r"^([^!]+)!([^@]+)@(.+)$";
return !mask.matchFirst(validMaskRegex).empty;
}
void listen(ushort port, string address)
{
listenTCP(port, &acceptConnection, address);
}
Connection[] findConnectionByNick(string nick)
{
return connections.find!(c => c.nick.toIRCLower == nick.toIRCLower);
}
bool canFindConnectionByNick(string nick)
{
return !findConnectionByNick(nick).empty;
}
bool isNickAvailable(string nick)
{
return !canFindConnectionByNick(nick);
}
Channel[] findChannelByName(string name)
{
return channels.find!(c => c.name.toIRCLower == name.toIRCLower);
}
bool canFindChannelByName(string name)
{
return !findChannelByName(name).empty;
}
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);
foreach (member; channel.members)
{
member.send(Message(connection.prefix, "JOIN", [channelName]));
}
channel.sendNames(connection);
if (!channel.topic.empty)
{
channel.sendTopic(connection);
}
}
void part(Connection connection, string channelName, string partMessage)
{
auto channel = connection.channels.array.find!(
c => c.name.toIRCLower == channelName.toIRCLower)[0];
channel.part(connection, partMessage);
if (channel.members.empty)
{
channels = channels.remove!(c => c == channel);
}
}
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;
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 whoChannel(Connection origin, string channelName, bool operatorsOnly)
{
//TODO: Check what RFCs say about secret/private channels
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);
}
}
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 privmsgToChannel(Connection sender, string target, string text)
{
auto channel = findChannelByName(target)[0];
channel.sendPrivMsg(sender, text);
}
void privmsgToUser(Connection sender, string target, string text)
{
auto user = findConnectionByNick(target)[0];
user.send(Message(sender.prefix, "PRIVMSG", [target, text], true));
}
void noticeToChannel(Connection sender, string target, string text)
{
auto channel = findChannelByName(target)[0];
channel.sendNotice(sender, text);
}
void noticeToUser(Connection sender, string target, string text)
{
auto user = findConnectionByNick(target)[0];
user.send(Message(sender.prefix, "NOTICE", [target, text], true));
}
void sendChannelTopic(Connection origin, string channelName)
{
auto channel = findChannelByName(channelName)[0];
channel.sendTopic(origin);
}
void setChannelTopic(Connection origin, string channelName, string newTopic)
{
auto channel = findChannelByName(channelName)[0];
channel.setTopic(origin, newTopic);
}
void sendChannelNames(Connection connection, string channelName)
{
auto channel = findChannelByName(channelName)[0];
channel.sendNames(connection);
}
void sendGlobalNames(Connection connection)
{
foreach (channel; channels.filter!(c => c.visibleTo(connection)))
{
channel.sendNames(connection, false);
}
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(' '));
}
connection.sendNumeric!RPL_ENDOFNAMES("*");
}
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();
}
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 sendVersion(Connection connection)
{
//TODO: Include enabled versions in comments?
connection.sendNumeric!RPL_VERSION(versionString ~ ".", name, ":");
}
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.");
}
}