package main import ( "bytes" "fmt" "io" "libnmdc" "net" "strings" "time" ) type Server struct { eventChan chan Event running bool name string client *Client channel Channel // Single blessed channel upstreamLauncher libnmdc.HubConnectionOptions upstream *libnmdc.HubConnection motd string } func NewServer(name string, upstream libnmdc.HubAddress) *Server { self := libnmdc.NewUserInfo("") self.ClientTag = APP_DESCRIPTION return &Server{eventChan: make(chan Event), name: name, client: nil, motd: "Connected to " + name, upstreamLauncher: libnmdc.HubConnectionOptions{ Address: upstream, Self: *self, }, channel: Channel{ clientMap: make(map[string]*Client), modeMap: make(map[string]*ClientMode), }, } } func (s *Server) RunClient(conn net.Conn) { s.client = &Client{ connection: conn, connected: true, server: s, // FIXME circular reference! } // Send the connection handshake s.client.sendGlobalMessage(s.motd) // Can't connect to the upstream server yet, until we've recieved a nick. for { buf := make([]byte, CLIENT_READ_BUFFSIZE) s.client.connection.SetReadDeadline(time.Now().Add(time.Second * CLIENT_READ_TIMEOUT_SEC)) ln, err := s.client.connection.Read(buf) if err != nil { if err == io.EOF { s.client.disconnect() return // FIXME cleanup } continue } rawLines := buf[:ln] rawLines = bytes.Replace(rawLines, []byte("\r\n"), []byte("\n"), -1) rawLines = bytes.Replace(rawLines, []byte("\r"), []byte("\n"), -1) lines := bytes.Split(rawLines, []byte("\n")) for _, line := range lines { if len(line) > 0 { // Client sent a command fields := strings.Fields(string(line)) if len(fields) < 1 { return } if strings.HasPrefix(fields[0], ":") { fields = fields[1:] } s.handleCommand(strings.ToUpper(fields[0]), fields[1:]) } } } } func (s *Server) ProtocolReadLoop_NMDC(closeChan chan struct{}) { // Initiate connection s.upstream = s.upstreamLauncher.Connect() // Read loop for { select { case <-closeChan: // Need some way of deliberately shutting down a libnmdc connection... return case hubEvent := <-s.upstream.OnEvent: switch hubEvent.EventType { case libnmdc.EVENT_USER_JOINED: s.client.reply(rplJoin, hubEvent.Nick, BLESSED_CHANNEL) case libnmdc.EVENT_USER_PART: s.client.reply(rplPart, hubEvent.Nick, BLESSED_CHANNEL, "Disconnected") case libnmdc.EVENT_USER_UPDATED_INFO: // description change - no relevance for IRC users case libnmdc.EVENT_CONNECTION_STATE_CHANGED: s.client.sendGlobalMessage("Upstream: " + hubEvent.StateChange.Format()) case libnmdc.EVENT_HUBNAME_CHANGED: s.client.reply(rplTopic, BLESSED_CHANNEL, hubEvent.Nick) // c.reply(rplNoTopic, BLESSED_CHANNEL) case libnmdc.EVENT_PRIVATE: s.client.reply(rplMsg, s.client.nick, hubEvent.Nick, hubEvent.Message) case libnmdc.EVENT_PUBLIC: s.client.reply(rplMsg, s.client.nick, BLESSED_CHANNEL, hubEvent.Message) case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB: s.client.sendGlobalMessage(hubEvent.Message) } } } } func (s *Server) handleCommand(command string, args []string) { switch command { case "PING": s.client.reply(rplPong) case "INFO": s.client.reply(rplInfo, APP_DESCRIPTION) case "VERSION": s.client.reply(rplVersion, VERSION) case "PASS": // RFC2812 registration // Stash the password for later if len(args) < 1 { s.client.reply(errMoreArgs) return } s.upstreamLauncher.NickPassword = args[0] case "NICK": if len(args) < 1 { s.client.reply(errMoreArgs) return } if s.upstreamLauncher.Self.Nick == "" { // allow set, as part of the login phase s.upstreamLauncher.Self.Nick = args[0] } else { s.client.reply(rplKill, "Can't change nicks on this server", "") s.client.disconnect() } case "USER": // This command sets altname, realname, ... none of which we use // It's the final step in a PASS/NICK/USER login handshake. if s.client.registered == true { s.client.reply(rplKill, "You're already registered.", "") s.client.disconnect() return } if s.client.nick == "" { s.client.reply(rplKill, "Your nickname is already being used", "") s.client.disconnect() return } s.client.reply(rplWelcome) s.client.registered = true // Spawn upstream connection go s.ProtocolReadLoop_NMDC(nil) // FIXME need shutdown synchronisation // Tell the user that they themselves joined the chat channel s.client.reply(rplJoin, s.client.nick, BLESSED_CHANNEL) case "JOIN": if s.client.registered == false { s.client.reply(errNotReg) return } if len(args) < 1 { s.client.reply(errMoreArgs) return } switch args[0] { case BLESSED_CHANNEL: // That's fine, but they're already there case "0": // Intend to quit all channels, but we don't allow that default: s.client.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "") s.client.disconnect() } case "PART": if s.client.registered == false { s.client.reply(errNotReg) return } // you can check out any time you like, but you can never leave // we'll need to transmit s.client.reply(rplPart, c.nick, channel.name, reason) messages on behalf of other nmdc users, though case "PRIVMSG": if s.client.registered == false { s.client.reply(errNotReg) return } if len(args) < 2 { s.client.reply(errMoreArgs) return } message := strings.Join(args[1:], " ") // IRC is case-insensitive case-preserving. We can respect that for the // channel name, but not really for user nicks if strings.ToLower(args[0]) == BLESSED_CHANNEL { s.upstream.SayPublic(message) } else if _, clientExists := s.upstream.Users[args[0]]; clientExists { s.upstream.SayPrivate(args[0], message) } else { s.client.reply(errNoSuchNick, args[0]) } case "QUIT": if s.client.registered == false { s.client.reply(errNotReg) return } s.client.disconnect() case "TOPIC": if s.client.registered == false { s.client.reply(errNotReg) return } if len(args) < 1 { s.client.reply(errMoreArgs) return } exists := strings.ToLower(args[0]) == BLESSED_CHANNEL if exists == false { s.client.reply(errNoSuchNick, args[0]) return } // Valid topic get if len(args) == 1 { s.client.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName) return } // Disallow topic set s.client.reply(errNoPriv) return case "LIST": if s.client.registered == false { s.client.reply(errNotReg) return } listItem := fmt.Sprintf("%s %d :%s", BLESSED_CHANNEL, len(s.channel.clientMap), s.upstream.HubName) s.client.reply(rplList, listItem) s.client.reply(rplListEnd) case "OPER": if s.client.registered == false { s.client.reply(errNotReg) return } if len(args) < 2 { s.client.reply(errMoreArgs) return } // Can't use this command. s.client.reply(errPassword) case "KILL": if s.client.registered == false { s.client.reply(errNotReg) return } s.client.reply(errNoPriv) return case "KICK": if s.client.registered == false { s.client.reply(errNotReg) return } s.client.reply(errNoPriv) return case "MODE": if s.client.registered == false { s.client.reply(errNotReg) return } if len(args) < 1 { s.client.reply(errMoreArgs) return } if strings.ToLower(args[0]) != BLESSED_CHANNEL { s.client.reply(errNoSuchNick, args[0]) return } if len(args) == 1 { // No more args, they just want the mode s.client.reply(rplChannelModeIs, args[0], BLESSED_CHANNEL_MODE, "") } else { // Setting modes is disallowed s.client.reply(errNoPriv) } return default: s.client.reply(errUnknownCommand, command) } }