2017-03-11 17:45:49 +01:00
module ircd.server ;
import std.stdio ;
import std.algorithm ;
import std.range ;
import std.conv ;
import std.socket ;
import core.time ;
2017-03-22 17:33:56 +01:00
import std.datetime ;
2017-04-07 08:08:24 +02:00
import std.string ;
2017-03-11 17:45:49 +01:00
import vibe.core.core ;
2020-02-11 15:25:14 +01:00
import vibe.core.net ;
2017-03-11 17:45:49 +01:00
import ircd.packageVersion ;
import ircd.message ;
import ircd.connection ;
2017-03-14 02:45:11 +01:00
import ircd.channel ;
2017-03-19 22:43:52 +01:00
import ircd.helpers ;
2017-03-11 17:45:49 +01:00
class Server
{
2020-02-11 15:01:08 +01:00
Connection [ ] connections ;
enum versionString = "salty-ircd-" ~ packageVersion ;
string name ;
enum string info = "A salty-ircd server" ; //TODO: Make server info configurable
string motd ;
Channel [ ] channels ;
private uint [ string ] _commandUsage ;
private ulong [ string ] _commandBytes ;
private string _pass = null ;
private SysTime _startTime ;
this ( )
{
name = Socket . hostName ;
readMotd ( ) ;
_startTime = Clock . currTime ;
runTask ( & pingLoop ) ;
}
private void readMotd ( )
{
import std.file : exists , readText ;
if ( exists ( "motd" ) )
{
motd = readText ( "motd" ) ;
}
}
private void pingLoop ( )
{
while ( true )
{
foreach ( connection ; connections )
{
connection . send ( Message ( null , "PING" , [ name ] , true ) ) ;
}
sleep ( 30. seconds ) ;
}
}
private void acceptConnection ( TCPConnection tcpConnection )
{
auto connection = new Connection ( tcpConnection , this ) ;
connections ~ = connection ;
connection . handle ( ) ;
connections = connections . filter ! ( c = > c ! = connection ) . array ;
}
static bool isValidChannelName ( string name )
{
return ( name . startsWith ( '#' ) | | name . startsWith ( '&' ) ) & & name . length < = 200 ;
}
static bool isValidNick ( string name )
{
import std.ascii : digits , letters ;
if ( name . length > 9 )
{
return false ;
}
foreach ( i , c ; name )
{
auto allowed = letters ~ "[]\\`_^{|}" ;
if ( i > 0 )
{
allowed ~ = digits ~ "-" ;
}
if ( ! allowed . canFind ( c ) )
{
return false ;
}
}
return true ;
}
static bool isValidUserMask ( string mask )
{
import std.regex : ctRegex , matchFirst ;
auto validMaskRegex = ctRegex ! r"^([^!]+)!([^@]+)@(.+)$" ;
return ! mask . matchFirst ( validMaskRegex ) . empty ;
}
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 . send ( Message ( name , "353" , [ connection . nick , "=" , "*" , otherUsers . map ! ( m = > m . nick ) . join ( ' ' ) ] , true ) ) ;
}
connection . sendRplEndOfNames ( "*" ) ;
}
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 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 sendVersion ( Connection connection )
{
connection . send ( Message ( name , "351" , [ connection . nick , versionString ~ "." , name , "" ] , true ) ) ;
}
void sendTime ( Connection connection )
{
auto timeString = Clock . currTime . toISOExtString ;
connection . send ( Message ( name , "391" , [ connection . nick , name , timeString ] , true ) ) ;
}
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 . 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 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 . send ( Message ( name , "251" , [ connection . nick , "There are " ~ connections . filter ! ( c = > c . registered ) . count . to ! string ~ " users and 0 services on 1 servers" ] , true ) ) ;
if ( connections . any ! ( c = > c . isOperator ) )
{
connection . send ( Message ( name , "252" , [ connection . nick , connections . count ! ( c = > c . isOperator ) . to ! string , "operator(s) online" ] , true ) ) ;
}
if ( connections . any ! ( c = > ! c . registered ) )
{
connection . send ( Message ( name , "253" , [ connection . nick , connections . count ! ( c = > ! c . registered ) . to ! string , "unknown connection(s)" ] , true ) ) ;
}
if ( channels . length > 0 )
{
connection . send ( Message ( name , "254" , [ connection . nick , channels . length . to ! string , "channels formed" ] , true ) ) ;
}
connection . send ( Message ( name , "255" , [ connection . nick , "I have " ~ connections . length . to ! string ~ " clients and 1 servers" ] , true ) ) ;
}
void ison ( Connection connection , string [ ] nicks )
{
auto reply = nicks . filter ! ( n = > canFindConnectionByNick ( n ) ) . join ( ' ' ) ;
connection . send ( Message ( name , "303" , [ connection . nick , reply ] , true ) ) ;
}
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 ) ) ;
//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 ) ) ;
if ( user . isOperator )
{
connection . send ( Message ( name , "313" , [ connection . nick , user . nick , "is an IRC operator" ] , true ) ) ;
}
auto idleSeconds = ( Clock . currTime - user . lastMessageTime ) . total ! "seconds" ;
connection . send ( Message ( name , "317" , [ connection . nick , user . nick , idleSeconds . to ! string , "seconds idle" ] , true ) ) ;
auto userChannels = user . channels . map ! ( c = > c . nickPrefix ( user ) ~ c . name ) . join ( ' ' ) ;
connection . send ( Message ( name , "319" , [ connection . nick , user . nick , userChannels ] , true ) ) ;
}
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 . send ( Message ( name , "212" , [ connection . nick , command , count . to ! string , _commandBytes [ command ] . to ! string , "0" ] , false ) ) ;
}
}
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 . send ( Message ( name , "242" , [ connection . nick , uptimeString ] , true ) ) ;
}
void setPass ( string pass )
{
_pass = pass ;
}
bool isPassCorrect ( string pass )
{
return pass = = _pass ;
}
void listen ( ushort port = 6667 )
{
listenTCP ( port , & acceptConnection ) ;
}
void listen ( ushort port , string address )
{
listenTCP ( port , & acceptConnection , address ) ;
}
2017-03-11 17:45:49 +01:00
}