diff --git a/client.go b/client.go index 1d4707e..8d74458 100644 --- a/client.go +++ b/client.go @@ -3,11 +3,11 @@ package main import ( "fmt" "net" - "strings" "time" ) type Client struct { + server *Server connection net.Conn nick string registered bool @@ -15,6 +15,7 @@ type Client struct { operator bool } +/* func (c *Client) joinChannel(channelName string) { newChannel := false @@ -29,18 +30,6 @@ func (c *Client) joinChannel(channelName string) { return } - // Send notifications to /other/ clients that /we/ joined this room - for _, client := range channel.clientMap { - client.reply(rplJoin, c.nick, BLESSED_CHANNEL) - } - - // Transmit topic - if channel.topic != "" { - c.reply(rplTopic, BLESSED_CHANNEL, channel.topic) - } else { - c.reply(rplNoTopic, BLESSED_CHANNEL) - } - // Transmit the list of joined users to us nicks := make([]string, 0, NICKS_PER_PROTOMSG) @@ -65,11 +54,26 @@ func (c *Client) joinChannel(channelName string) { c.reply(rplEndOfNames, channelName) } +*/ func (c *Client) disconnect() { c.connected = false } +func (c *Client) sendGlobalMessage(motd string) { + c.reply(rplMOTDStart) + + for len(motd) > 80 { + c.reply(rplMOTD, motd[:80]) + motd = motd[80:] + } + if len(motd) > 0 { + c.reply(rplMOTD, motd) + } + + c.reply(rplEndOfMOTD) +} + //Send a reply to a user with the code specified func (c *Client) reply(code replyCode, args ...string) { if c.connected == false { @@ -96,6 +100,7 @@ func (c *Client) reply(code replyCode, args ...string) { case rplKill: c.write(fmt.Sprintf(":%s KILL %s A %s", args[0], c.nick, args[1])) case rplMsg: + // FIXME escape newlines in message!! c.write(fmt.Sprintf(":%s PRIVMSG %s %s", args[0], args[1], args[2])) case rplList: c.write(fmt.Sprintf(":%s 322 %s %s", c.server.name, c.nick, args[0])) diff --git a/server.go b/server.go index dbefefd..4a6d3ad 100644 --- a/server.go +++ b/server.go @@ -7,26 +7,28 @@ import ( "libnmdc" "net" "strings" - "time" ) type Server struct { - eventChan chan Event - running bool - name string - client *Client - channel Channel // Single blessed channel - upstreamAddr libnmdc.HubAddress - upstream libnmdc.HubConnection - motd string + 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 { return &Server{eventChan: make(chan Event), - name: name, - client: nil, - motd: "Connected to " + name, - upstreamAddr: upstream, + name: name, + client: nil, + motd: "Connected to " + name, + upstreamLauncher: libnmdc.HubConnectionOptions{ + Address: upstream, + Self: *libnmdc.NewUserInfo(""), + }, channel: Channel{ clientMap: make(map[string]*Client), modeMap: make(map[string]*ClientMode), @@ -39,105 +41,175 @@ 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.reply(rplMOTDStart) - motd := s.motd - for len(motd) > 80 { - s.client.reply(rplMOTD, motd[:80]) - motd = motd[80:] - } - if len(motd) > 0 { - s.client.reply(rplMOTD, motd) - } - s.client.reply(rplEndOfMOTD) + s.client.sendGlobalMessage(s.motd) + + // Can't connect to the upstream server yet, until we've recieved a nick. + // So we can't have a conjoined select statement + // Need a separate goroutine for both IRC and NMDC protocols, and + // synchronisation to ensure simultaneous cleanup + + closeChan := make(chan struct{}, 2) + go s.ProtocolReadLoop_IRC(closeChan) + // FIXME + +} + +func (s *Server) ProtocolReadLoop_IRC(closeChan chan struct{}) { + + defer func() { + closeChan <- struct{}{} + }() // Read loop for { - s.client.connection.SetReadDeadline(time.Now().Add(time.Second * CLIENT_READ_TIMEOUT_SEC)) buf := make([]byte, CLIENT_READ_BUFFSIZE) - ln, err := s.client.connection.Read(buf) - if err != nil { - if err == io.EOF { - s.client.disconnect() - return // FIXME cleanup - } - continue - } + // s.client.connection.SetReadDeadline(time.Now().Add(time.Second * CLIENT_READ_TIMEOUT_SEC)) - 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 { - s.handleCommandEvent(string(line)) + select { + case <-closeChan: + return + + case 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) handleCommandEvent(input string) { +func (s *Server) ProtocolReadLoop_NMDC(closeChan chan struct{}) { + defer func() { + closeChan <- struct{}{} + }() - //Client send a command - fields := strings.Fields(input) - if len(fields) < 1 { - return + // Read loop + for { + select { + case <-closeChan: + 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) + + } + } } - - if strings.HasPrefix(fields[0], ":") { - fields = fields[1:] - } - command := strings.ToUpper(fields[0]) - args := fields[1:] - - s.handleCommand(s.client, command, args) - } -func (s *Server) handleCommand(client *Client, command string, args []string) { +func (s *Server) handleCommand(command string, args []string) { switch command { case "PING": - client.reply(rplPong) + s.client.reply(rplPong) case "INFO": - client.reply(rplInfo, APP_DESCRIPTION) + s.client.reply(rplInfo, APP_DESCRIPTION) case "VERSION": - client.reply(rplVersion, 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": - client.reply(rplKill, "Can't change nicks on this server", "") - client.disconnect() - - case "USER": - if client.registered == true { - client.reply(rplKill, "You're already registered.", "") - client.disconnect() + if len(args) < 1 { + s.client.reply(errMoreArgs) + return } - if client.nick == "" { - client.reply(rplKill, "Your nickname is already being used", "") - client.disconnect() + 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": + if s.client.registered == true { + s.client.reply(rplKill, "You're already registered.", "") + s.client.disconnect() + } + + if s.client.nick == "" { + s.client.reply(rplKill, "Your nickname is already being used", "") + s.client.disconnect() } else { - client.reply(rplWelcome) - client.registered = true + s.client.reply(rplWelcome) + s.client.registered = true - // Spawn + // Spawn upstream connection } case "JOIN": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } if len(args) < 1 { - client.reply(errMoreArgs) + s.client.reply(errMoreArgs) return } @@ -147,27 +219,27 @@ func (s *Server) handleCommand(client *Client, command string, args []string) { case "0": // Intend to quit all channels, but we don't allow that default: - client.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "") - client.disconnect() + s.client.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "") + s.client.disconnect() } case "PART": - if client.registered == false { - client.reply(errNotReg) + 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 client.reply(rplPart, c.nick, channel.name, reason) messages on behalf of other nmdc users, though + // 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 client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } if len(args) < 2 { - client.reply(errMoreArgs) + s.client.reply(errMoreArgs) return } @@ -178,75 +250,78 @@ func (s *Server) handleCommand(client *Client, command string, args []string) { recipient := strings.ToLower(args[0]) if recipient == BLESSED_CHANNEL { - for _, c := range s.channel.clientMap { - if c != client { - c.reply(rplMsg, client.nick, args[0], message) + s.upstream.SayPublic(message) + /* + for _, c := range s.channel.clientMap { + if c != client { + c.reply(rplMsg, s.client.nick, args[0], message) + } } - } + */ } else if nmdcUser, clientExists := s.upstream.Users[args[0]]; clientExists { s.upstream.SayPrivate(recipient, message) - // client2.reply(rplMsg, client.nick, client2.nick, message) + // client2.reply(rplMsg, s.client.nick, client2.nick, message) } else { - client.reply(errNoSuchNick, args[0]) + s.client.reply(errNoSuchNick, args[0]) } case "QUIT": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } - client.disconnect() + s.client.disconnect() case "TOPIC": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } if len(args) < 1 { - client.reply(errMoreArgs) + s.client.reply(errMoreArgs) return } exists := strings.ToLower(args[0]) == BLESSED_CHANNEL if exists == false { - client.reply(errNoSuchNick, args[0]) + s.client.reply(errNoSuchNick, args[0]) return } // Valid topic get if len(args) == 1 { - client.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName) + s.client.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName) return } // Disallow topic set - client.reply(errNoPriv) + s.client.reply(errNoPriv) return case "LIST": - if client.registered == false { - client.reply(errNotReg) + 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) - client.reply(rplList, listItem) - client.reply(rplListEnd) + s.client.reply(rplList, listItem) + s.client.reply(rplListEnd) case "OPER": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } if len(args) < 2 { - client.reply(errMoreArgs) + s.client.reply(errMoreArgs) return } @@ -254,59 +329,59 @@ func (s *Server) handleCommand(client *Client, command string, args []string) { //password := args[1] if false { // op the user - client.operator = true - client.reply(rplOper) + s.client.operator = true + s.client.reply(rplOper) return } else { - client.reply(errPassword) + s.client.reply(errPassword) } case "KILL": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } - client.reply(errNoPriv) + s.client.reply(errNoPriv) return case "KICK": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } - client.reply(errNoPriv) + s.client.reply(errNoPriv) return case "MODE": - if client.registered == false { - client.reply(errNotReg) + if s.client.registered == false { + s.client.reply(errNotReg) return } if len(args) < 1 { - client.reply(errMoreArgs) + s.client.reply(errMoreArgs) return } if strings.ToLower(args[0]) != BLESSED_CHANNEL { - client.reply(errNoSuchNick, args[0]) + s.client.reply(errNoSuchNick, args[0]) return } if len(args) == 1 { //No more args, they just want the mode - client.reply(rplChannelModeIs, args[0], BLESSED_CHANNEL_MODE, "") + s.client.reply(rplChannelModeIs, args[0], BLESSED_CHANNEL_MODE, "") } else { // Setting modes is disallowed - client.reply(errNoPriv) + s.client.reply(errNoPriv) } return default: - client.reply(errUnknownCommand, command) + s.client.reply(errUnknownCommand, command) } }