diff --git a/main.go b/main.go index ec6cba0..8f07efe 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ func main() { log.SetLevel(logrus.DebugLevel) log.Println("sircd starting up") s := sircd.NewSircd() + s.SetServerName("irc.example.com") s.SetLogger(log) go s.Start() for s.Running { diff --git a/sircd/client.go b/sircd/client.go index 175c8df..ce1a883 100644 --- a/sircd/client.go +++ b/sircd/client.go @@ -5,20 +5,25 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" "net" + "strings" + "fmt" ) -func newIrcClient(conn net.Conn, log *logrus.Logger, mc chan *ircMessage) *ircClient { +func newIrcClient(conn net.Conn, log *logrus.Logger, mc chan *ircMessage, s *sircd) *ircClient { c := new(ircClient) c.Id = uuid.New().String() c.conn = conn c.log = log c.mc = mc + c.server = s c.inputBytes = new(bytes.Buffer) c.outputBytes = new(bytes.Buffer) + c.session = NewIrcUserSession() return c } type ircClient struct { + //FIXME add a mutex and protect during writes Id string conn net.Conn log *logrus.Logger @@ -26,11 +31,93 @@ type ircClient struct { inputBytes *bytes.Buffer outputBytes *bytes.Buffer mc chan *ircMessage + server *sircd } -func (c *ircClient) Close() { +func (c *ircClient) ServerName() string { + return c.server.serverName +} + +func (c *ircClient) Kill() { + //server will remove it from the server-managed client list + //and will call c.CleanupAndClose() when it pops from this channel + c.server.deadClients <- c +} + +func (c *ircClient) CleanupAndClose() { //client cleanup - c.log.Infof("client %d disconnect", c.Id) + //FIXME truncate client buffers and whatnot here + // server object already handles removing it from the list of clients + // for the server, so don't do it here + c.log.Infof("client=%s cleanup", c.Id) + c.conn.Close() +} + +func (c *ircClient) AppendStringToOutputBuffer(input string) { + // woo boilerplate because no polymorphism + c.AppendBytesToOutputBuffer([]byte(input)) +} + +func (c *ircClient) AppendBytesToOutputBuffer(input []byte) { + c.outputBytes.Write(input) +} + +func (c *ircClient) FlushOutputBuffer() bool { + numBytes, err := c.outputBytes.WriteTo(c.conn) + if err != nil { + c.log.Debugf("failed to write completely to client<%s>, marking dead", c.Id) + c.Kill() + return false + } else { + c.log.Debugf("wrote %d bytes to client<%s>", numBytes, c.Id) + return true + } +} + +func (c *ircClient) RespUnknownCommand(command string) { + //421 == unknown command + c.SendServerMessage(421, []string{command, "Unknown command. Please try your call again later."}) +} + +func (c *ircClient) SendServerMessage(code uint16, params []string) bool { + //FIXME this is terrible hacky duct tape code, redo it + p := "" + if len(params) > 0 { + // pop off last item for separate processing + lastelem := params[len(params)-1] + params = params[:len(params)-1] + + for _, elem := range params { + if strings.ContainsAny(elem, " \t\r\n") { + c.log.Errorf("wtf %s", elem) + return false + } + } + + if strings.ContainsAny(lastelem, "\t\r\n") { + c.log.Errorf("wtf %s", lastelem) + return false + } + + if strings.ContainsAny(lastelem, " ") { + lastelem = fmt.Sprintf(":%s", lastelem) + } + p = strings.Join(params, " ") + if len(p) > 0 { + p = " " + lastelem + } else { + p = lastelem + } + } + + sendstr := fmt.Sprintf(":%s %s %d", c.ServerName(), code, c.session.nick) + if len(p) > 0 { + sendstr = sendstr + " " + p + } + sendstr = sendstr + "\r\n" + c.log.Debugf("sending string '%s' to client<%s>", sendstr, c.Id) + c.AppendStringToOutputBuffer(sendstr) + return c.FlushOutputBuffer() } func (c *ircClient) AppendInputBuffer(input []byte) { @@ -44,16 +131,17 @@ func (c *ircClient) AppendInputBuffer(input []byte) { func (c *ircClient) ParseInputBuffer() { c.log.Debugf("my input buffer is %d bytes", c.inputBytes.Len()) c.log.Debugf("my input buffer is: '%s'", c.inputBytes.String()) - for line, err := c.inputBytes.ReadString(byte('\n')) - if err == nil { - c.mc <- c.ParseSingleInput(line) //FIXME make this return val, err - } else { - c.log.Debugf("error parsing input buffer: ", err.Error()) - } + line, err := c.inputBytes.ReadString(byte('\n')) + if err == nil { + c.mc <- c.ParseSingleInput(line) + c.ParseInputBuffer() + } else { + c.log.Debugf("error parsing input buffer: ", err.Error()) + } c.log.Debugf("my input buffer is %d bytes", c.inputBytes.Len()) c.log.Debugf("my input buffer is: '%s'", c.inputBytes.String()) } func (c *ircClient) ParseSingleInput(line string) *ircMessage { - return NewIrcMessageFromString(line, c) + return NewIrcMessageFromString(line, c) } diff --git a/sircd/irc.go b/sircd/irc.go new file mode 100644 index 0000000..5d5fdf4 --- /dev/null +++ b/sircd/irc.go @@ -0,0 +1,35 @@ +package sircd + +func (s *sircd) processIRCMessage(m *ircMessage) { + switch m.command { + case "CAP": + s.processCAPCommand(m) + case "NICK": + s.processNICKCommand(m) + case "USER": + s.processUSERCommand(m) + default: + s.processUnknownCommand(m) + } + s.log.Infof(m.String()) + s.log.Infof("%+v", m) +} + +func (s *sircd) processUnknownCommand(m *ircMessage) { + m.from.RespUnknownCommand(m.command) +} + +func (s *sircd) processNICKCommand(m *ircMessage) { + //FIXME check if nick is in use + //FIXME check if nick is valid + m.from.session.SetNick(m.params[0]) +} + +func (s *sircd) processUSERCommand(m *ircMessage) { + m.from.session.SetUserInfo(m.params[0]) +} + +func (s *sircd) processCAPCommand(m *ircMessage) { + s.log.Debugln("ignoring CAP command, unsupported") + // pass +} diff --git a/sircd/main.go b/sircd/main.go deleted file mode 100644 index 582246a..0000000 --- a/sircd/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package sircd - -func (s *sircd) processIRCMessage(m *ircMessage) { - s.log.Info("not implemented") - //for client, _ := range s.ircClients { - //} -} - -/* - -import "net" - case publish := <-publishes: - for conn, _ := range conns { - go func(conn net.Conn) { - totalWritten := 0 - for totalWritten < len(publish) { - writtenThisCall, err := conn.Write(publish[totalWritten:]) - if err != nil { - deadConns <- conn - break - } - totalWritten += writtenThisCall - } - }(conn) - } - } - } - listener.Close() -} -*/ diff --git a/sircd/message.go b/sircd/message.go index b55230b..d768f20 100644 --- a/sircd/message.go +++ b/sircd/message.go @@ -1,65 +1,69 @@ package sircd import ( - "strings" - "fmt" + "errors" + "fmt" "regexp" + "strings" ) - type ircMessage struct { from *ircClient //received - tags string - source string - raw string + tags string + source string + raw string command string - params []string + params []string } -func NewIrcMessageFromString (line string, from *ircClient) *ircMessage { +func parseIrcLine(line string) (*ircMessage, error) { + //FIXME do this at compile or start time instead of every message + ircregex, err := regexp.Compile(`^(\@(\S+) )?(?:[:](\S+) )?(\S+)(?: ([^:].+?))?(?: [:](.+))?$`) - //FIXME do this at compile or start time instead of every message - ircregex, err := regexp.Compile(`^(\@(\S+) )?(?:[:](\S+) )?(\S+)(?: ([^:].+?))?(?: [:](.+))?$`) + if err != nil { + panic("can't happen") + } - if err != nil { - panic("can't happen") - } + line = strings.TrimRight(line, "\r\n") - line = strings.TrimRight(line, "\r\n") - m := new(ircMessage) - m.raw = line + m := new(ircMessage) + m.raw = line - if ircregex.MatchString(m.raw) == false { - m.command = "UNKNOWN" - return m - } - matches := ircregex.FindAllStringSubmatch(m.raw, -1) + if ircregex.MatchString(m.raw) == false { + return nil, errors.New("parse error") + } + matches := ircregex.FindAllStringSubmatch(m.raw, -1) - if len(matches) == 0 { - m.command = "UNKNOWN" - return m - } + if len(matches) == 0 { + return nil, errors.New("parse error") + } - match := matches[0] - fmt.Printf("%+v\n", match) - fmt.Printf("%+v\n", len(match)) - m.tags = match[2] - m.source = match[3] - m.command = match[4] - if len(match[5]) > 0 { - m.params = strings.Fields(match[5]) - } - if len(match[6]) > 0 { - m.params = append(m.params, match[6]) - } + match := matches[0] + fmt.Printf("%+v\n", match) + fmt.Printf("%+v\n", len(match)) + m.tags = match[2] + m.source = match[3] + m.command = strings.ToUpper(match[4]) + if len(match[5]) > 0 { + m.params = strings.Fields(match[5]) + } + if len(match[6]) > 0 { + m.params = append(m.params, match[6]) + } - fmt.Printf("%+v\n", &m) - fmt.Println(m) - return m + return m, nil } +func NewIrcMessageFromString(line string, from *ircClient) *ircMessage { + msg, err := parseIrcLine(line) + + if err != nil { + panic("wat") + } + return msg +} func (m *ircMessage) String() string { - return fmt.Sprintf("IRCMessage<%s>('%s')", m.command, m.raw) + return fmt.Sprintf("IRCMessage<%s>('%s')", m.command, strings.Join(m.params, ",")) } diff --git a/sircd/server.go b/sircd/server.go index a129c10..582a85b 100644 --- a/sircd/server.go +++ b/sircd/server.go @@ -7,6 +7,7 @@ import ( type sircd struct { Running bool + serverName string log *logrus.Logger netName string ircPort uint16 @@ -31,8 +32,12 @@ func NewSircd() *sircd { return s } -func (s *sircd) SetLogger(l *logrus.Logger) { - s.log = l +func (s *sircd) SetLogger(logger *logrus.Logger) { + s.log = logger +} + +func (s *sircd) SetServerName(name string) { + s.serverName = name } func (s *sircd) Start() { @@ -53,7 +58,7 @@ func (s *sircd) Start() { if err != nil { s.log.Panicf("Error accepting: ", err.Error()) } - s.newClients <- newIrcClient(conn, s.log, s.messageQueue) + s.newClients <- newIrcClient(conn, s.log, s.messageQueue, s) }() go s.mainloop() @@ -70,7 +75,8 @@ func (s *sircd) mainloop() { for { nbyte, err := client.conn.Read(buf) if err != nil { - s.deadClients <- client + //this will surface it in the deadClients channel + client.Kill() break } else { fragment := make([]byte, nbyte) @@ -82,29 +88,9 @@ func (s *sircd) mainloop() { }() case deadClient := <-s.deadClients: delete(s.ircClients, deadClient) - deadClient.Close() + deadClient.CleanupAndClose() case message := <-s.messageQueue: s.processIRCMessage(message) } } } - -/* - case publish := <-publishes: - for conn, _ := range conns { - go func(conn net.Conn) { - totalWritten := 0 - for totalWritten < len(publish) { - writtenThisCall, err := conn.Write(publish[totalWritten:]) - if err != nil { - deadConns <- conn - break - } - totalWritten += writtenThisCall - } - }(conn) - } - } - } - listener.Close() -*/ diff --git a/sircd/session.go b/sircd/session.go index 2883bc4..ffe1fcd 100644 --- a/sircd/session.go +++ b/sircd/session.go @@ -2,8 +2,24 @@ package sircd type ircNick string type ircRealName string +type hostname string type ircUserSession struct { + //FIXME add a mutex and protect during writes nick ircNick realname ircRealName + host hostname +} + +func NewIrcUserSession() *ircUserSession { + // FIXME get conn.RemoteAddr passed in and stringify it here and put it + // in the session + s := new(ircUserSession) + s.nick = '*' //default for s2c messages pre-NICK + return s +} + +func (s *ircUserSession) SetNick(input string) { + // FIXME check for valid nick-ness + s.nick = input }