diff --git a/server.go b/server.go new file mode 100644 index 0000000..6efd0d2 --- /dev/null +++ b/server.go @@ -0,0 +1,442 @@ +// 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 + + 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.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) + } + sv.dispatcher() +} + +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() { + 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 { + sv.sendReply(cl.Name(), ERR_NICKNAMEINUSE, "", "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()) { + sv.sendReply(cl.Name(), 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) + } + } +} + +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") +} + +func (sv *Server) channelNames(nick, ch string) { + chid := strings.ToLower(ch) + if _, exists := sv.chUsers[chid]; !exists { + return + } + names := "" + for clid, mode := range sv.chUsers[chid] { + name := sv.clients[clid].Name() + if names != "" { + names += " " + } + names = names + mode + name + } + sv.sendReply(nick, RPL_NAMEREPLY, "= "+ch, names) + sv.sendReply(nick, RPL_ENDOFNAMES, ch, "End of /NAMES list") +} + +type commandHook struct { + HookFn func(sv *Server, msg *irc.Message) + MinArgs int + NeedTrail bool + NeedOper bool + NeedAuth bool +} + +var svCommandHooks = map[string]commandHook{ + "PRIVMSG": {handleCmdPrivmsg, 1, true, false, false}, + "JOIN": {handleCmdJoin, 1, false, false, false}, + "PART": {handleCmdPart, 1, false, false, false}, + "QUIT": {handleCmdQuit, 0, false, false, false}, + "MODE": {handleCmdMode, 1, false, false, false}, + "TOPIC": {handleCmdTopic, 1, false, false, false}, + "NAMES": {handleCmdNames, 1, false, false, false}, + "WHOIS": {handleCmdWhois, 0, false, false, false}, + "PING": {handleCmdPing, 1, false, false, false}, + "REHASH": {handleCmdRehash, 0, false, false, false}, + /* + "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) { + sv.sendMsg(msg) +} + +func handleCmdJoin(sv *Server, msg *irc.Message) { + 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) + } + 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]) +} + +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) + +} + +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]) + if _, exists := sv.chUsers[chid]; !exists { + return + } + if len(msg.Args) < 2 { + return + } + modeFlag := strings.ToLower(msg.Args[1]) + if len(msg.Args) < 3 { + //modeTar := "" + } else { + //modeTar := strings.ToLower(msg.Args[2]) + } + switch modeFlag { + case "+o": + case "-o": + case "+h": + case "-h": + case "+b": + case "-b": + case "+v": + case "-v": + } + sv.sendMsg(msg) + } +} + +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") +}