package main
import (
"bufio"
"crypto/tls"
"encoding/base64"
"fmt"
"log"
"math/rand"
"net"
"os"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
"gopkg.in/sorcix/irc.v2"
)
const (
COMMAND_HELP = "HELP"
COMMAND_INFO = "INFO"
// User commands
COMMAND_REGISTER = "REGISTER"
COMMAND_IDENTIFY = "IDENTIFY"
COMMAND_TOKEN = "TOKEN"
COMMAND_USERNAME = "USERNAME"
COMMAND_PASSWORD = "PASSWORD"
// User/channel commands
COMMAND_MODE = "MODE"
// Channel/server commands
COMMAND_FOUND = "FOUND"
COMMAND_DROP = "DROP"
COMMAND_GRANT = "GRANT"
COMMAND_REVEAL = "REVEAL"
COMMAND_KICK = "KICK"
COMMAND_BAN = "BAN"
COMMAND_AUDIT = "AUDIT"
// Server admin commands
COMMAND_KILL = "KILL"
COMMAND_STATS = "STATS"
COMMAND_REHASH = "REHASH"
COMMAND_UPGRADE = "UPGRADE"
)
var serverCommands = [ ] string { COMMAND_KILL , COMMAND_STATS , COMMAND_REHASH , COMMAND_UPGRADE }
// TODO: Reorder
const (
PERMISSION_CLIENT = 0
PERMISSION_REGISTERED = 1
PERMISSION_VIP = 2
PERMISSION_MODERATOR = 3
PERMISSION_ADMIN = 4
PERMISSION_SUPERADMIN = 5
)
var permissionLabels = map [ int ] string {
PERMISSION_CLIENT : "Client" ,
PERMISSION_REGISTERED : "Registered Client" ,
PERMISSION_VIP : "VIP" ,
PERMISSION_MODERATOR : "Moderator" ,
PERMISSION_ADMIN : "Administrator" ,
PERMISSION_SUPERADMIN : "Super Administrator" ,
}
var ALL_PERMISSIONS = "Client, Registered Client, VIP, Moderator, Administrator and Super Administrator"
var commandRestrictions = map [ int ] [ ] string {
PERMISSION_REGISTERED : { COMMAND_TOKEN , COMMAND_USERNAME , COMMAND_PASSWORD , COMMAND_FOUND } ,
PERMISSION_MODERATOR : { COMMAND_MODE , COMMAND_REVEAL , COMMAND_KICK , COMMAND_BAN } ,
PERMISSION_ADMIN : { COMMAND_GRANT , COMMAND_AUDIT } ,
PERMISSION_SUPERADMIN : { COMMAND_DROP , COMMAND_KILL , COMMAND_STATS , COMMAND_REHASH , COMMAND_UPGRADE } }
var helpDuration = "Duration can be 0 to never expire, or e.g. 30m, 1h, 2d, 3w"
var commandUsage = map [ string ] [ ] string {
COMMAND_HELP : { "[command|all]" ,
"Print usage information regarding a specific command or 'all'" ,
"Without a command or 'all', only commands you have permission to use are printed" } ,
COMMAND_INFO : { "[channel]" ,
"When a channel is specified, prints info including whether it is registered" ,
"Without a channel, server info is printed" } ,
COMMAND_REGISTER : { "<username> <password>" ,
"Create an account" ,
"Once you've registered, other users may GRANT permissions to you, or " ,
"See IDENTIFY" } ,
COMMAND_IDENTIFY : { "[username] <password>" ,
"Identify to a previously registered account" ,
"If username is omitted, it will be replaced with your current nick" ,
"Note that you may automatically identify when connecting by specifying a server password of your username and password separated by a colon - Example: admin:hunter2" } ,
COMMAND_TOKEN : { "<channel>" ,
"Returns a token which can be used by channel administrators to grant special access to your account" } ,
COMMAND_USERNAME : { "<username> <password> <new username> <confirm new username>" ,
"Change your username" } ,
COMMAND_PASSWORD : { "<username> <password> <new password> <confirm new password>" ,
"Change your password" } ,
COMMAND_FOUND : { "<channel>" ,
"Take ownership of an unfounded channel" } ,
COMMAND_GRANT : { "<channel> [account] [permission]" ,
"When an account token isn't specified, all accounts with permissions are listed" ,
"Specify an account token and permission level to grant that permission" ,
"Specify an account token only to view that account's permission" ,
"To remove an account's permissions, set their permission to Client" ,
"Permissions: " + ALL_PERMISSIONS } ,
COMMAND_REVEAL : { "<channel> [page] [all]" ,
"Print channel log, allowing KICK/BAN to be used" ,
fmt . Sprintf ( "Results start at page 1, %d per page" , CHANNEL_LOGS_PER_PAGE ) ,
"Page -1 shows all matching entries" ,
"Joins and parts are hidden by default, add 'all' to show them" } ,
COMMAND_AUDIT : { "<channel> [page]" ,
"Print channel audit log" ,
fmt . Sprintf ( "Results start at page 1, %d per page" , CHANNEL_LOGS_PER_PAGE ) ,
"Page -1 shows all matching entries" } ,
COMMAND_KICK : { "<channel> <5 digit log number> [reason]" ,
"Kick a user from a channel" } ,
COMMAND_BAN : { "<channel> <5 digit log number> <duration> [reason]" ,
"Kick and ban a user from a channel" ,
helpDuration } ,
COMMAND_DROP : { "<channel> <confirm channel>" ,
"Delete all channel data, allowing it to be founded again" } ,
COMMAND_KILL : { "<channel> <5 digit log number> <duration> [reason]" ,
"Disconnect and ban a user from the server" ,
helpDuration } ,
COMMAND_STATS : { "" ,
"Print the current number of clients and channels" } ,
COMMAND_REHASH : { "" ,
"Reload the server configuration" } ,
COMMAND_UPGRADE : { "" ,
"Upgrade the server without disconnecting clients" } ,
}
type Config struct {
MOTD string
Salt string
DBDriver string
DBSource string
SSLCert string
SSLKey string
}
type Server struct {
config * Config
configfile string
created int64
motd [ ] string
clients * sync . Map
channels * sync . Map
odyssey * os . File
odysseymutex * sync . RWMutex
restartplain chan bool
restartssl chan bool
* sync . RWMutex
}
var db = & Database { }
func NewServer ( configfile string ) * Server {
s := & Server { }
s . config = & Config { }
s . configfile = configfile
s . created = time . Now ( ) . Unix ( )
s . clients = new ( sync . Map )
s . channels = new ( sync . Map )
s . odysseymutex = new ( sync . RWMutex )
s . restartplain = make ( chan bool , 1 )
s . restartssl = make ( chan bool , 1 )
s . RWMutex = new ( sync . RWMutex )
return s
}
func ( s * Server ) hashPassword ( username string , password string ) string {
sha512 := sha3 . New512 ( )
_ , err := sha512 . Write ( [ ] byte ( strings . Join ( [ ] string { username , s . config . Salt , password } , "-" ) ) )
if err != nil {
return ""
}
return base64 . URLEncoding . EncodeToString ( sha512 . Sum ( nil ) )
}
func ( s * Server ) getAnonymousPrefix ( i int ) * irc . Prefix {
prefix := prefixAnonymous
if i > 1 {
prefix . Name += fmt . Sprintf ( "%d" , i )
}
return & prefix
}
func ( s * Server ) getChannel ( channel string ) * Channel {
if ch , ok := s . channels . Load ( channel ) ; ok {
return ch . ( * Channel )
}
return nil
}
func ( s * Server ) getChannels ( client string ) map [ string ] * Channel {
channels := make ( map [ string ] * Channel )
s . channels . Range ( func ( k , v interface { } ) bool {
key := k . ( string )
channel := v . ( * Channel )
if client == "" || s . inChannel ( key , client ) {
channels [ key ] = channel
}
return true
} )
return channels
}
func ( s * Server ) channelCount ( ) int {
i := 0
s . channels . Range ( func ( k , v interface { } ) bool {
i ++
return true
} )
return i
}
func ( s * Server ) getClient ( client string ) * Client {
if cl , ok := s . clients . Load ( client ) ; ok {
return cl . ( * Client )
}
return nil
}
func ( s * Server ) getClients ( channel string ) map [ string ] * Client {
clients := make ( map [ string ] * Client )
if channel == "" {
s . clients . Range ( func ( k , v interface { } ) bool {
cl := s . getClient ( k . ( string ) )
if cl != nil {
clients [ cl . identifier ] = cl
}
return true
} )
return clients
}
ch := s . getChannel ( channel )
if ch == nil {
return clients
}
ch . clients . Range ( func ( k , v interface { } ) bool {
cl := s . getClient ( k . ( string ) )
if cl != nil {
clients [ cl . identifier ] = cl
}
return true
} )
return clients
}
func ( s * Server ) clientCount ( ) int {
i := 0
s . clients . Range ( func ( k , v interface { } ) bool {
i ++
return true
} )
return i
}
func ( s * Server ) revealClient ( channel string , identifier string ) * Client {
riphash , raccount := s . revealClientInfo ( channel , identifier )
if riphash == "" && raccount == 0 {
return nil
}
cls := s . getClients ( "" )
for _ , rcl := range cls {
if rcl . iphash == riphash || ( rcl . account > 0 && rcl . account == raccount ) {
return rcl
}
}
return nil
}
func ( s * Server ) revealClientInfo ( channel string , identifier string ) ( string , int64 ) {
if len ( identifier ) != 5 {
return "" , 0
}
ch := s . getChannel ( channel )
if ch == nil {
return "" , 0
}
return ch . RevealInfo ( identifier )
}
func ( s * Server ) inChannel ( channel string , client string ) bool {
ch := s . getChannel ( channel )
if ch != nil {
_ , ok := ch . clients . Load ( client )
return ok
}
return false
}
func ( s * Server ) canJoin ( c * Client , channel string , key string ) ( bool , string ) {
dbch , err := db . Channel ( channel )
if err != nil || dbch . Channel == "" {
return false , "invalid channel"
}
ch := s . getChannel ( channel )
if ch == nil {
return false , "invalid channel"
} else if banned , reason := c . isBanned ( channel ) ; banned {
if reason != "" {
reason = fmt . Sprintf ( " (%s)" , reason )
}
return false , "you are banned" + reason
} else if ch . hasMode ( "z" ) && ! c . ssl {
return false , "only clients connected via SSL are allowed"
}
permission := c . globalPermission ( )
requiredPermission := PERMISSION_CLIENT
reason := ""
if channel [ 0 ] == '&' {
if permission < PERMISSION_VIP {
return false , "restricted channel"
}
} else if channel [ 0 ] != '#' {
return false , "invalid channel"
}
if permission < requiredPermission && c . account > 0 {
chp , err := db . GetPermission ( c . account , channel )
if err != nil && chp . Permission > permission {
permission = chp . Permission
}
}
if permission < PERMISSION_VIP {
if ch . hasMode ( "k" ) && ch . getMode ( "k" ) != key {
return false , "invalid channel key specified"
} else if ch . hasMode ( "l" ) {
var l int
var err error
l , err = strconv . Atoi ( ch . getMode ( "l" ) )
if err != nil {
l = 0
}
if l > 0 && ch . clientCount ( ) >= l {
return false , "limited channel, join again in a few moments"
}
}
}
if ch . hasMode ( "r" ) {
requiredPermission = PERMISSION_REGISTERED
reason = "only registered clients are allowed"
}
if ch . hasMode ( "i" ) {
requiredPermission = PERMISSION_VIP
reason = "only VIP are allowed"
}
return permission >= requiredPermission , reason
}
func ( s * Server ) joinChannel ( client string , channel string , key string ) {
if s . inChannel ( channel , client ) {
return // Already in channel
}
cl := s . getClient ( client )
if cl == nil {
return
}
ch := s . getChannel ( channel )
if ch == nil {
ch = NewChannel ( channel )
s . channels . Store ( channel , ch )
} else if canaccess , reason := s . canJoin ( cl , channel , key ) ; ! canaccess {
errmsg := fmt . Sprintf ( "Cannot join %s: %s" , channel , reason )
cl . writeMessage ( irc . ERR_INVITEONLYCHAN , [ ] string { channel , errmsg } )
cl . sendNotice ( errmsg )
return
}
ch . clients . Store ( client , s . anonCount ( channel , client ) + 1 )
cl . write ( cl . getPrefix ( ) , irc . JOIN , [ ] string { channel } )
ch . Log ( cl , irc . JOIN , "" )
s . sendNames ( channel , client )
s . updateClientCount ( channel , client , "" )
s . sendTopic ( channel , client , false )
}
func ( s * Server ) partChannel ( channel string , client string , reason string ) {
ch := s . getChannel ( channel )
cl := s . getClient ( client )
if cl == nil || ! s . inChannel ( channel , client ) {
return
}
cl . write ( cl . getPrefix ( ) , irc . PART , [ ] string { channel , reason } )
ch . Log ( cl , irc . PART , reason )
ch . clients . Delete ( client )
s . updateClientCount ( channel , client , reason )
// TODO: Destroy empty channel
}
func ( s * Server ) partAllChannels ( client string , reason string ) {
for channelname := range s . getChannels ( client ) {
s . partChannel ( channelname , client , reason )
}
}
func ( s * Server ) revealChannelLog ( channel string , client string , page int , showAll bool ) {
cl := s . getClient ( client )
if cl == nil {
return
}
ch := s . getChannel ( channel )
if ch == nil {
cl . sendError ( "Unable to reveal, invalid channel specified" )
return
} else if ! ch . HasClient ( client ) {
cl . sendError ( "Unable to reveal, you are not in that channel" )
return
}
r := ch . RevealLog ( page , showAll )
for _ , rev := range r {
cl . sendMessage ( rev )
}
}
func ( s * Server ) enforceModes ( channel string ) {
ch := s . getChannel ( channel )
if ch != nil && ch . hasMode ( "z" ) {
for client , cl := range s . getClients ( channel ) {
if ! cl . ssl {
s . partChannel ( channel , client , fmt . Sprintf ( "You must connect via SSL to join %s" , channel ) )
}
}
}
}
func ( s * Server ) anonCount ( channel string , client string ) int {
ch := s . getChannel ( channel )
cl := s . getClient ( client )
if ch == nil || cl == nil {
return 0
}
ccount := ch . clientCount ( )
if ( ch . hasMode ( "c" ) || cl . hasMode ( "c" ) ) && ccount >= 2 {
return 2
}
return ccount
}
func ( s * Server ) updateClientCount ( channel string , client string , reason string ) {
ch := s . getChannel ( channel )
if ch == nil {
return
}
var reasonShown bool
ch . clients . Range ( func ( k , v interface { } ) bool {
cl := s . getClient ( k . ( string ) )
ccount := v . ( int )
if cl == nil {
return true
} else if client != "" && ch . hasMode ( "D" ) && cl . identifier != client {
return true
}
reasonShown = false
chancount := s . anonCount ( channel , cl . identifier )
if ccount < chancount {
for i := ccount ; i < chancount ; i ++ {
cl . write ( s . getAnonymousPrefix ( i ) , irc . JOIN , [ ] string { channel } )
}
ch . clients . Store ( cl . identifier , chancount )
} else if ccount > chancount {
for i := ccount ; i > chancount ; i -- {
pr := ""
if ! reasonShown {
pr = reason
}
cl . write ( s . getAnonymousPrefix ( i - 1 ) , irc . PART , [ ] string { channel , pr } )
reasonShown = true
}
} else {
return true
}
ch . clients . Store ( cl . identifier , chancount )
return true
} )
}
func ( s * Server ) sendNames ( channel string , clientname string ) {
if ! s . inChannel ( channel , clientname ) {
return
}
cl := s . getClient ( clientname )
if cl == nil {
return
}
names := [ ] string { }
if cl . capHostInNames {
names = append ( names , cl . getPrefix ( ) . String ( ) )
} else {
names = append ( names , cl . nick )
}
ccount := s . anonCount ( channel , clientname )
for i := 1 ; i < ccount ; i ++ {
if cl . capHostInNames {
names = append ( names , s . getAnonymousPrefix ( i ) . String ( ) )
} else {
names = append ( names , s . getAnonymousPrefix ( i ) . Name )
}
}
cl . writeMessage ( irc . RPL_NAMREPLY , [ ] string { "=" , channel , strings . Join ( names , " " ) } )
cl . writeMessage ( irc . RPL_ENDOFNAMES , [ ] string { channel , "End of /NAMES list." } )
}
func ( s * Server ) sendTopic ( channel string , client string , changed bool ) {
if ! s . inChannel ( channel , client ) {
return
}
ch := s . getChannel ( channel )
cl := s . getClient ( client )
if ch == nil || cl == nil {
return
}
tprefix := prefixAnonymous
tcommand := irc . TOPIC
if ! changed {
tprefix = prefixAnonIRC
tcommand = irc . RPL_TOPIC
}
cl . write ( & tprefix , tcommand , [ ] string { channel , ch . topic } )
if ! changed {
cl . writeMessage ( strings . Join ( [ ] string { irc . RPL_TOPICWHOTIME , cl . nick , channel , prefixAnonymous . Name , fmt . Sprintf ( "%d" , ch . topictime ) } , " " ) , nil )
}
}
func ( s * Server ) handleTopic ( channel string , client string , topic string ) {
ch := s . getChannel ( channel )
cl := s . getClient ( client )
if ch == nil || cl == nil {
return
}
if ! s . inChannel ( channel , client ) {
cl . sendNotice ( "Invalid use of TOPIC" )
return
}
chp , err := db . GetPermission ( cl . account , channel )
if err != nil {
log . Panicf ( "%+v" , err )
} else if ch . hasMode ( "t" ) && chp . Permission < PERMISSION_VIP {
cl . accessDenied ( PERMISSION_VIP )
return
}
ch . topic = topic
ch . topictime = time . Now ( ) . Unix ( )
ch . clients . Range ( func ( k , v interface { } ) bool {
s . sendTopic ( channel , k . ( string ) , true )
return true
} )
ch . Log ( cl , irc . TOPIC , ch . topic )
}
func ( s * Server ) handleChannelMode ( c * Client , params [ ] string ) {
ch := s . getChannel ( params [ 0 ] )
if ch == nil || ! s . inChannel ( params [ 0 ] , c . identifier ) {
return
}
if len ( params ) == 1 || params [ 1 ] == "" {
c . writeMessage ( strings . Join ( [ ] string { irc . RPL_CHANNELMODEIS , c . nick , params [ 0 ] , ch . printModes ( ch . getModes ( ) , nil ) } , " " ) , [ ] string { } )
// Send channel creation time
c . writeMessage ( strings . Join ( [ ] string { "329" , c . nick , params [ 0 ] , fmt . Sprintf ( "%d" , int32 ( ch . created ) ) } , " " ) , [ ] string { } )
} else if len ( params ) > 1 && ( params [ 1 ] == "" || params [ 1 ] [ 0 ] == '+' || params [ 1 ] [ 0 ] == '-' ) {
if ! c . canUse ( COMMAND_MODE , params [ 0 ] ) {
c . accessDenied ( c . permissionRequired ( COMMAND_MODE ) )
return
}
lastmodes := make ( map [ string ] string )
for m , mv := range ch . getModes ( ) {
lastmodes [ m ] = mv
}
if params [ 1 ] [ 0 ] == '+' {
ch . addModes ( params [ 1 : ] )
} else {
ch . removeModes ( params [ 1 ] [ 1 : ] )
}
s . enforceModes ( params [ 0 ] )
if reflect . DeepEqual ( ch . getModes ( ) , lastmodes ) {
return
}
// TODO: Check if local modes were set/unset, only send changes to local client
addedmodes , removedmodes := ch . diffModes ( lastmodes )
resendusercount := false
if _ , ok := addedmodes [ "c" ] ; ok {
resendusercount = true
}
if _ , ok := removedmodes [ "c" ] ; ok {
resendusercount = true
}
if _ , ok := removedmodes [ "D" ] ; ok {
resendusercount = true
}
if len ( addedmodes ) == 0 && len ( removedmodes ) == 0 {
addedmodes = c . getModes ( )
}
ch . clients . Range ( func ( k , v interface { } ) bool {
cl := s . getClient ( k . ( string ) )
if cl != nil {
cl . write ( & prefixAnonymous , irc . MODE , [ ] string { params [ 0 ] , ch . printModes ( addedmodes , removedmodes ) } )
}
return true
} )
if resendusercount {
s . updateClientCount ( params [ 0 ] , c . identifier , "Enforcing MODEs" )
}
}
}
func ( s * Server ) handleUserMode ( c * Client , params [ ] string ) {
if len ( params ) == 1 || params [ 1 ] == "" {
c . writeMessage ( strings . Join ( [ ] string { irc . RPL_UMODEIS , c . nick , c . printModes ( c . getModes ( ) , nil ) } , " " ) , [ ] string { } )
return
}
lastmodes := c . getModes ( )
if len ( params ) > 1 && len ( params [ 1 ] ) > 0 && ( params [ 1 ] [ 0 ] == '+' || params [ 1 ] [ 0 ] == '-' ) {
if params [ 1 ] [ 0 ] == '+' {
c . addModes ( params [ 1 : ] )
} else {
c . removeModes ( params [ 1 ] [ 1 : ] )
}
}
if reflect . DeepEqual ( c . modes , lastmodes ) {
return
}
addedmodes , removedmodes := c . diffModes ( lastmodes )
resendusercount := false
if _ , ok := addedmodes [ "c" ] ; ok {
resendusercount = true
}
if _ , ok := removedmodes [ "c" ] ; ok {
resendusercount = true
}
if _ , ok := removedmodes [ "D" ] ; ok {
resendusercount = true
}
if len ( addedmodes ) == 0 && len ( removedmodes ) == 0 {
addedmodes = c . getModes ( )
}
c . writeMessage ( strings . Join ( [ ] string { irc . MODE , c . nick } , " " ) , [ ] string { c . printModes ( addedmodes , removedmodes ) } )
if resendusercount {
for ch := range s . getChannels ( c . identifier ) {
s . updateClientCount ( ch , c . identifier , "Enforcing MODEs" )
}
}
}
func ( s * Server ) handleMode ( c * Client , params [ ] string ) {
if c == nil {
return
}
if len ( params ) == 0 || len ( params [ 0 ] ) == 0 {
c . sendNotice ( "Invalid use of MODE" )
return
}
if len ( params ) > 1 && params [ 1 ] == "b" {
c . writeMessage ( irc . RPL_ENDOFBANLIST , [ ] string { params [ 0 ] , "End of Channel Ban List" } )
return
}
if params [ 0 ] [ 0 ] == '#' || params [ 0 ] [ 0 ] == '&' {
s . handleChannelMode ( c , params )
} else {
s . handleUserMode ( c , params )
}
}
func ( s * Server ) sendUsage ( cl * Client , command string ) {
command = strings . ToUpper ( command )
showAll := false
if command == "ALL" {
command = COMMAND_HELP
showAll = true
}
commands := make ( [ ] string , 0 , len ( commandUsage ) )
for cmd := range commandUsage {
commands = append ( commands , cmd )
}
sort . Strings ( commands )
var printedLabel bool
var usage [ ] string
if command == COMMAND_HELP {
// Print all commands
var perms [ ] int