// vim:ts=4:sts=4:sw=4:noet:tw=72 package ircd import ( "fmt" "net" "os" "runtime" "strings" "time" "code.dnix.de/an/conf" "code.dnix.de/an/irc" "code.dnix.de/an/xlog" ) const ( CHANLIMIT = 1024 CHANNELLEN = 200 TOPICLEN = 1024 ) var myinfo string = "%s %s/%s * *" var isupport string = "CASEMAPPING=rfc1459 CHANTYPES=# NICKLEN=32 PREFIX=(aohv)&@%%+" type Server struct { queue chan *irc.Message addq chan Client delq chan Client host string info string software string version string created string motd string clients map[string]Client clModes map[string]string chUsers map[string]map[string]string chTopics map[string]string chModes map[string]map[string]bool config *conf.ConfigFile configPath string packetsTransferred float64 connectionsCurrent float64 connectionsCount float64 queueLen float64 authCallback func(name, pass string) bool } // Create a new server instance. func NewServer(configPath, software, version string) *Server { sv := &Server{software: software, version: version, created: time.Now().String()} sv.queue = make(chan *irc.Message, 1024) sv.addq = make(chan Client, 128) sv.delq = make(chan Client, 128) sv.clients = make(map[string]Client) sv.clModes = make(map[string]string) sv.chUsers = make(map[string]map[string]string) sv.chTopics = make(map[string]string) sv.chModes = make(map[string]map[string]bool) sv.configPath = configPath sv.loadConfig() loglevel, _ := sv.config.GetInt("system", "loglevel") xlog.Init(loglevel) sv.host, _ = sv.config.GetString("server", "host") sv.info, _ = sv.config.GetString("server", "info") sv.motd, _ = sv.config.GetString("server", "motd") sv.packetsTransferred = 0 sv.connectionsCurrent = 0 sv.connectionsCount = 0 sv.queueLen = 0 return sv } func (sv *Server) SetAuthCallback(authCB func(name, pass string) bool) { sv.authCallback = authCB } // Open the listening port and start the main server loop. func (sv *Server) Run() { xlog.Info("%s/%s", sv.software, sv.version) go monitoringRun(sv) laddr, err := sv.config.GetString("net", "listen") if err == nil { go sv.listen(laddr) } laddr, err = sv.config.GetString("net", "listen_tls") if err == nil { go sv.listenTls(laddr) } for { err = sv.dispatcher() if err != nil { xlog.Error("Dispatcher in panic: %s", err.Error()) } } } func (sv *Server) Dispatch(msg *irc.Message) { sv.queue <- msg } func (sv *Server) AddClient(cl Client) { sv.addq <- cl } func (sv *Server) DelClient(cl Client) { sv.delq <- cl } func (sv *Server) listen(laddr string) { listen, err := net.Listen("tcp", laddr) if err != nil { xlog.Fatal(err.Error()) os.Exit(-1) } for { time.Sleep(1 * time.Millisecond) conn, err := listen.Accept() if err != nil { xlog.Error(err.Error()) } else { NewRemoteClient(sv, conn) sv.connectionsCount++ } } } func (sv *Server) listenTls(laddr string) { } func (sv *Server) dispatcher() (err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }() for { sv.queueLen = float64(len(sv.queue)) select { case msg := <-sv.queue: sv.recvMsg(msg) sv.packetsTransferred++ case cl := <-sv.addq: clid := strings.ToLower(cl.Name()) if _, exists := sv.clients[clid]; exists { cl.Receive(irc.M(sv.host, ERR_NICKNAMEINUSE, cl.Name(), "Nickname is already in use")) go func() { time.Sleep(5 * time.Second) cl.Register(false) }() xlog.Info("Client registration failed: '%s' (client exists)", clid) continue } if !sv.authCallback(cl.Name(), cl.Password()) { cl.Receive(irc.M(sv.host, ERR_PASSWDMISMATCH, "", "Password incorrect")) go func() { time.Sleep(5 * time.Second) cl.Register(false) }() xlog.Info("Client registration failed: '%s' (wrong password)", clid) continue } sv.clients[clid] = cl sv.clients[clid] = cl sv.sendLogon(cl.Name()) sv.connectionsCurrent = float64(len(sv.clients)) cl.Register(true) xlog.Info("Client registered: '%s'", clid) xlog.Info("Server has %d client(s)", len(sv.clients)) xlog.Debug("Goroutines running: %d", runtime.NumGoroutine()) case cl := <-sv.delq: clid := strings.ToLower(cl.Name()) cl.Destroy() for chname, ch := range sv.chUsers { if _, exists := ch[clid]; exists { delete(ch, clid) sv.sendMsg(irc.M(cl.Name(), "PART", chname, "quit")) } } delete(sv.clients, clid) sv.connectionsCurrent = float64(len(sv.clients)) xlog.Info("Client deleted: '%s'", clid) xlog.Info("Server has %d client(s)", len(sv.clients)) xlog.Debug("Goroutines running: %d", runtime.NumGoroutine()) default: time.Sleep(100 * time.Microsecond) } } return } func (sv *Server) loadConfig() { cfg, err := conf.ReadConfigFile(sv.configPath) if err != nil { xlog.Fatal("Can't read config file (%s)", err.Error()) os.Exit(-1) } sv.config = cfg } func (sv *Server) recvMsg(msg *irc.Message) { cmd := msg.Cmd hook, exists := svCommandHooks[cmd] if !exists { sv.sendReply(msg.Pre, ERR_UNKNOWNCOMMAND, cmd, "Unknown command") return } argc := len(msg.Args) if argc < hook.MinArgs { sv.sendReply(msg.Pre, ERR_NEEDMOREPARAMS, cmd, "Not enough parameters") return } if hook.NeedTrail && msg.Trail == "" { sv.sendReply(msg.Pre, ERR_NEEDMOREPARAMS, cmd, "Not enough parameters") return } hook.HookFn(sv, msg) } func (sv *Server) sendMsg(msg *irc.Message) { if strings.HasPrefix(msg.Args[0], "#") { chid := strings.ToLower(msg.Args[0]) if _, exists := sv.chUsers[chid]; !exists { sv.sendReply(msg.Pre, ERR_NOSUCHNICK, msg.Args[0], "No such nick/channel") return } for clid, _ := range sv.chUsers[chid] { if strings.ToLower(msg.Pre) == clid && msg.Cmd == "PRIVMSG" { continue } if cl, exists := sv.clients[clid]; exists { cl.Receive(msg) } } } else { clid := strings.ToLower(msg.Args[0]) if _, exists := sv.clients[clid]; !exists { sv.sendReply(msg.Pre, ERR_NOSUCHNICK, msg.Args[0], "No such nick/channel") return } cl := sv.clients[clid] cl.Receive(msg) } } func (sv *Server) sendReply(nick, cmd, args, trail string) { clid := strings.ToLower(nick) if _, exists := sv.clients[clid]; !exists { return } cl := sv.clients[clid] if args != "" { args = nick + " " + args } else { args = nick } cl.Receive(irc.M(sv.host, cmd, args, trail)) } func (sv *Server) sendLogon(nick string) { sv.sendReply(nick, RPL_WELCOME, "", "Willkommen!") sv.sendReply(nick, RPL_YOURHOST, "", fmt.Sprintf("Your host is %s, running on %s/%s", sv.host, sv.software, sv.version)) sv.sendReply(nick, RPL_CREATED, "", fmt.Sprintf("This server was created %s", sv.created)) sv.sendReply(nick, RPL_MYINFO, "", fmt.Sprintf(myinfo, sv.host, sv.software, sv.version)) sv.sendReply(nick, RPL_ISUPPORT, "", isupport+" are supported by this server") sv.sendReply(nick, RPL_MOTDSTART, "", fmt.Sprintf("- %s Message of the day -", sv.host)) for _, line := range strings.Split(sv.motd, "\n") { sv.sendReply(nick, RPL_MOTD, "", fmt.Sprintf("- %s", line)) } sv.sendReply(nick, RPL_ENDOFMOTD, "", "End of MOTD command") } // Send channel name list to client func (sv *Server) channelNames(nick, ch string) { chid := strings.ToLower(ch) if _, exists := sv.chUsers[chid]; !exists { return } names := "" for clid, _ := range sv.chUsers[chid] { name := sv.clients[clid].Name() if names != "" { names += " " } modeFlags := "ohv" modeChars := "@%+" for i, mf := range modeFlags { if _, exists := sv.chModes[chid][string(mf)+" "+clid]; exists { name = string(modeChars[i]) + name break } } names = names + name } chunks := len(strings.Split(names, " "))/8 + 1 nameList := strings.SplitN(names, " ", chunks) for _, nameSlice := range nameList { sv.sendReply(nick, RPL_NAMEREPLY, "= "+ch, nameSlice) } sv.sendReply(nick, RPL_ENDOFNAMES, ch, "End of /NAMES list") } // Check if mode exists for the specified channel func (sv *Server) channelCheckMode(chid, mode string) bool { if _, exists := sv.chModes[chid]; !exists { return false } if _, exists := sv.chModes[chid][mode]; !exists { return false } return true } type commandHook struct { HookFn func(sv *Server, msg *irc.Message) MinArgs int NeedTrail bool NeedMode string } var svCommandHooks = map[string]commandHook{ "PRIVMSG": {handleCmdPrivmsg, 1, true, ""}, "JOIN": {handleCmdJoin, 1, false, ""}, "PART": {handleCmdPart, 1, false, ""}, "QUIT": {handleCmdQuit, 0, false, ""}, "MODE": {handleCmdMode, 1, false, ""}, "TOPIC": {handleCmdTopic, 1, false, ""}, "NAMES": {handleCmdNames, 1, false, ""}, "WHOIS": {handleCmdWhois, 0, false, ""}, "PING": {handleCmdPing, 1, false, ""}, "REHASH": {handleCmdRehash, 0, false, "ao"}, /* "LIST": {handleCmdList, 0, false, false}, "VERSION": {handleCmdVersion, 0, false, false}, "STATS": {handleCmdStats, 0, false, false}, "TIME": {handleCmdTime, 0, false, false}, "OPER": {handleCmdOper, 1, false, false}, "ADMIN": {handleCmdAdmin, 0, false, false}, "INFO": {handleCmdInfo, 0, false, false}, "WHO": {handleCmdWho, 0, false, false}, "WHOWAS": {handleCmdWhowas, 0, false, false}, "KILL": {handleCmdKill, 0, false, false}, "PONG": {handleCmdPong, 0, false, false}, "ERROR": {handleCmdError, 0, false, false}, "AWAY": {handleCmdAway, 0, false, false}, "RESTART": {handleCmdRestart, 0, false, false}, "SUMMON": {handleCmdSummon, 0, false, false}, "USERS": {handleCmdUsers, 0, false, false}, "USERHOST": {handleCmdUserhost, 0, false, false}, "ISON": {handleCmdIson, 0, false, false}, */ } func handleCmdPrivmsg(sv *Server, msg *irc.Message) { if strings.HasPrefix(msg.Args[0], "#") { clid := strings.ToLower(msg.Pre) chid := strings.ToLower(msg.Args[0]) if sv.channelCheckMode(chid, "m") && !sv.channelCheckMode(chid, "v "+clid) { return } } sv.sendMsg(msg) } func handleCmdJoin(sv *Server, msg *irc.Message) { first := false clid := strings.ToLower(msg.Pre) chid := strings.ToLower(msg.Args[0]) if _, exists := sv.chUsers[chid]; !exists { sv.chUsers[chid] = make(map[string]string) sv.chTopics[chid] = "" sv.chModes[chid] = make(map[string]bool) first = true } if _, exists := sv.chUsers[chid][clid]; exists { return } sv.chUsers[chid][clid] = "" sv.sendMsg(msg) sv.sendReply(msg.Pre, RPL_TOPIC, msg.Args[0], sv.chTopics[msg.Args[0]]) sv.channelNames(msg.Pre, msg.Args[0]) if !first { return } mode := "o " + clid sv.chModes[chid][mode] = true sv.sendMsg(irc.M(sv.host, "MODE", chid+" +o "+clid, "")) } // Handle user parting the channel and delete channel if last user has // left func handleCmdPart(sv *Server, msg *irc.Message) { clid := strings.ToLower(msg.Pre) chid := strings.ToLower(msg.Args[0]) if _, exists := sv.chUsers[chid]; !exists { return } if _, exists := sv.chUsers[chid][clid]; !exists { return } sv.sendMsg(msg) delete(sv.chUsers[chid], clid) delete(sv.chModes[chid], "o "+clid) delete(sv.chModes[chid], "h "+clid) delete(sv.chModes[chid], "v "+clid) if len(sv.chUsers[chid]) == 0 { delete(sv.chUsers, chid) delete(sv.chTopics, chid) delete(sv.chModes, chid) } } func handleCmdQuit(sv *Server, msg *irc.Message) { } func handleCmdMode(sv *Server, msg *irc.Message) { if strings.HasPrefix(msg.Args[0], "#") { chid := strings.ToLower(msg.Args[0]) clid := strings.ToLower(msg.Pre) if _, exists := sv.chUsers[chid]; !exists { return } if len(msg.Args) < 2 { return } modeFlag := msg.Args[1] if len(msg.Args) < 3 { //modeTar := "" } else { //modeTar := strings.ToLower(msg.Args[2]) } switch modeFlag { // maybe this can be done more elegant case "+o": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } case "-o": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } case "+h": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } case "-h": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } case "+b": if !sv.channelCheckMode(chid, "o "+clid) && !sv.channelCheckMode(chid, "h "+clid) { goto noPerm } case "-b": if !sv.channelCheckMode(chid, "o "+clid) && !sv.channelCheckMode(chid, "h "+clid) { goto noPerm } case "+v": if !sv.channelCheckMode(chid, "o "+clid) && !sv.channelCheckMode(chid, "h "+clid) { goto noPerm } case "-v": if !sv.channelCheckMode(chid, "o "+clid) && !sv.channelCheckMode(chid, "h "+clid) { goto noPerm } case "+m": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } case "-m": if !sv.channelCheckMode(chid, "o "+clid) { goto noPerm } } sv.sendMsg(msg) return noPerm: sv.sendReply(clid, ERR_CHANOPRIVSNEEDED, chid, "You're not channel operator") } } func handleCmdTopic(sv *Server, msg *irc.Message) { ch := msg.Args[0] if _, exists := sv.chUsers[ch]; !exists { sv.sendReply(msg.Pre, ERR_NOSUCHCHANNEL, ch, "No such channel") } if msg.Trail == "" { sv.sendReply(msg.Pre, RPL_TOPIC, ch, sv.chTopics[ch]) } else { sv.chTopics[ch] = msg.Trail sv.sendMsg(msg) //sv.sendReply(msg.Pre, RPL_TOPIC, ch, msg.Trail) } } func handleCmdNames(sv *Server, msg *irc.Message) { ch := msg.Args[0] if _, exists := sv.chUsers[ch]; !exists { sv.sendReply(msg.Pre, ERR_NOSUCHCHANNEL, ch, "No such channel") } sv.channelNames(msg.Pre, ch) } func handleCmdWhois(sv *Server, msg *irc.Message) { sv.sendReply(msg.Pre, RPL_WHOISUSER, "nick user host *", "real name") } func handleCmdPing(sv *Server, msg *irc.Message) { sv.sendReply(msg.Pre, "PONG", msg.Args[0], "") } func handleCmdRehash(sv *Server, msg *irc.Message) { sv.loadConfig() sv.sendReply(msg.Pre, RPL_REHASHING, "", "Rehashing.") xlog.Info("Rehashing") }