package main import ( "fmt" "log" "net" "regexp" "strings" "golang.org/x/crypto/bcrypt" ) var ( nickRegexp = regexp.MustCompile(`^[a-zA-Z\[\]_^{|}][a-zA-Z0-9\[\]_^{|}]*$`) channelRegexp = regexp.MustCompile(`^#[a-zA-Z0-9_\-]+$`) ) func NewServer() *Server { return &Server{eventChan: make(chan Event), name: "", clientMap: make(map[string]*Client), channelMap: make(map[string]*Channel), operatorMap: make(map[string][]byte), motd: "", } } func (s *Server) Run() { for event := range s.eventChan { s.handleEvent(event) } } func (s *Server) HandleConnection(conn net.Conn) { client := &Client{server: s, connection: conn, outputChan: make(chan string), signalChan: make(chan signalCode, 3), channelMap: make(map[string]*Channel), connected: true, } go client.clientThread() } func (s *Server) handleEvent(e Event) { defer func(event Event) { err := recover() if err != nil { log.Printf("Recovered from error when handling event: %+v", event) log.Println(err) } }(e) switch e.event { case connected: //Client connected e.client.reply(rplMOTDStart) motd := s.motd for len(motd) > 80 { e.client.reply(rplMOTD, motd[:80]) motd = motd[80:] } if len(motd) > 0 { e.client.reply(rplMOTD, motd) } e.client.reply(rplEndOfMOTD) case disconnected: //Client disconnected case command: //Client send a command fields := strings.Fields(e.input) if len(fields) < 1 { return } if strings.HasPrefix(fields[0], ":") { fields = fields[1:] } command := strings.ToUpper(fields[0]) args := fields[1:] s.handleCommand(e.client, command, args) } } func (s *Server) handleCommand(client *Client, command string, args []string) { switch command { case "PING": client.reply(rplPong) case "INFO": client.reply(rplInfo, APP_DESCRIPTION) case "VERSION": client.reply(rplVersion, VERSION) case "NICK": client.reply(rplKill, "Can't change nicks on this server", "") client.disconnect() case "USER": if client.nick == "" { client.reply(rplKill, "Your nickname is already being used", "") client.disconnect() } else { client.reply(rplWelcome) client.registered = true } case "JOIN": if client.registered == false { client.reply(errNotReg) return } if len(args) < 1 { client.reply(errMoreArgs) return } if args[0] == "0" { // Intend to quit all channels // But we don't allow that return } channels := strings.Split(args[0], ",") for _, channel := range channels { //Join the channel if it's valid if channelRegexp.MatchString(channel) { client.joinChannel(channel) } } case "PART": if client.registered == false { client.reply(errNotReg) return } // you can check out any time you like, but you can never leave // we'll need to transmit client.reply(rplPart, c.nick, channel.name, reason) messages on behalf of other nmdc users, though case "PRIVMSG": if client.registered == false { client.reply(errNotReg) return } if len(args) < 2 { client.reply(errMoreArgs) return } message := strings.Join(args[1:], " ") channel, chanExists := s.channelMap[strings.ToLower(args[0])] client2, clientExists := s.clientMap[strings.ToLower(args[0])] if chanExists { if channel.mode.noExternal { if _, inChannel := channel.clientMap[client.key]; !inChannel { //Not in channel, not allowed to send client.reply(errCannotSend, args[0]) return } } if channel.mode.moderated { clientMode := channel.modeMap[client.key] if !clientMode.operator && !clientMode.voice { //It's moderated and we're not +v or +o, do nothing client.reply(errCannotSend, args[0]) return } } for _, c := range channel.clientMap { if c != client { c.reply(rplMsg, client.nick, args[0], message) } } } else if clientExists { client2.reply(rplMsg, client.nick, client2.nick, message) } else { client.reply(errNoSuchNick, args[0]) } case "QUIT": if client.registered == false { client.reply(errNotReg) return } client.disconnect() case "TOPIC": if client.registered == false { client.reply(errNotReg) return } if len(args) < 1 { client.reply(errMoreArgs) return } channel, exists := s.channelMap[strings.ToLower(args[0])] if exists == false { client.reply(errNoSuchNick, args[0]) return } // Valid topic get if len(args) == 1 { client.reply(rplTopic, channel.name, s.name) return } // Disallow topic set client.reply(errNoPriv) return case "LIST": if client.registered == false { client.reply(errNotReg) return } if len(args) == 0 { for channelName, channel := range s.channelMap { if channel.mode.secret { if _, inChannel := channel.clientMap[client.key]; !inChannel { //Not in the channel, skip continue } } listItem := fmt.Sprintf("%s %d :%s", channelName, len(channel.clientMap), channel.topic) client.reply(rplList, listItem) } client.reply(rplListEnd) } else { channels := strings.Split(args[0], ",") for _, channelName := range channels { if channel, exists := s.channelMap[strings.ToLower(channelName)]; exists { listItem := fmt.Sprintf("%s %d :%s", channelName, len(channel.clientMap), channel.topic) client.reply(rplList, listItem) } } client.reply(rplListEnd) } case "OPER": if client.registered == false { client.reply(errNotReg) return } if len(args) < 2 { client.reply(errMoreArgs) return } username := args[0] password := args[1] if hashedPassword, exists := s.operatorMap[username]; exists { //nil means the passwords matched if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)); err == nil { client.operator = true client.reply(rplOper) return } } client.reply(errPassword) case "KILL": if client.registered == false { client.reply(errNotReg) return } if client.operator == false { client.reply(errNoPriv) return } if len(args) < 1 { client.reply(errMoreArgs) return } nick := args[0] reason := strings.Join(args[1:], " ") client, exists := s.clientMap[strings.ToLower(nick)] if !exists { client.reply(errNoSuchNick, nick) return } client.reply(rplKill, client.nick, reason) client.disconnect() case "KICK": if client.registered == false { client.reply(errNotReg) return } client.reply(errNoPriv) return case "MODE": if client.registered == false { client.reply(errNotReg) return } if len(args) < 1 { client.reply(errMoreArgs) return } channelKey := strings.ToLower(args[0]) channel, channelExists := s.channelMap[channelKey] if !channelExists { client.reply(errNoSuchNick, args[0]) return } mode := channel.mode if len(args) == 1 { //No more args, they just want the mode client.reply(rplChannelModeIs, args[0], mode.String(), "") return } // Setting modes is disallowed client.reply(errNoPriv) return default: client.reply(errUnknownCommand, command) } }