diff --git a/goircd.go b/goircd.go index 822d34bd..ab968579 100644 --- a/goircd.go +++ b/goircd.go @@ -1,5 +1,5 @@ package main -// http://tools.ietf.org/html/rfc1459 +// http://tools.ietf.org/html/rfc2812 import ( "irc" @@ -7,5 +7,5 @@ import ( func main() { server := irc.NewServer() - server.Listen(":6697") + server.Listen(":6667") } diff --git a/src/irc/client.go b/src/irc/client.go index e5370924..245b75e2 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -1,28 +1,47 @@ package irc import ( + "log" "net" + "strings" ) +type Message struct { + command string + args string + client *Client +} type Client struct { - conn net.Conn - ch chan Message + addr net.Addr + send chan string + recv chan string + username string + realname string + nick string } func NewClient(conn net.Conn) *Client { - return &Client{conn, NewMessageChan(NewStringChan(conn))} + client := new(Client) + client.addr = conn.RemoteAddr() + client.send = StringWriteChan(conn) + client.recv = StringReadChan(conn) + return client } -// Write messages from the client to the server. -func (c *Client) Communicate(server chan Message) { - for message := range c.ch { - message.client = c - server <- message +// Adapt `chan string` to a `chan Message`. +func (c *Client) Communicate(server *Server) { + go func() { + for str := range c.recv { + parts := strings.SplitN(str, " ", 2) + server.Send(Message{parts[0], parts[1], c}) + } + }() +} + +func (c *Client) Send(lines ...string) { + for _, line := range lines { + log.Printf("C <- S: %s", line) + c.send <- line } - c.Close() -} - -func (c *Client) Close() { - c.conn.Close() } diff --git a/src/irc/net.go b/src/irc/net.go index fcbfa874..360dcd02 100644 --- a/src/irc/net.go +++ b/src/irc/net.go @@ -3,43 +3,42 @@ package irc import ( "bufio" "log" + "strings" "net" ) // Adapt `net.Conn` to a `chan string`. -func NewStringChan(conn net.Conn) chan string { +func StringReadChan(conn net.Conn) chan string { ch := make(chan string) - rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - - done := make(chan bool) - go func() { - <- done - close(ch) - }() - - // conn -> ch + reader := bufio.NewReader(conn) go func() { for { - line, err := rw.ReadString('\n') + line, err := reader.ReadString('\n') + if (line != "") { + ch <- strings.TrimSpace(line) + } if err != nil { - log.Print("StringChan[read]: %v", err) + log.Print("StringReadChan[read]: ", err) break } - ch <- line } - done <- true + close(ch) }() + return ch +} - // ch -> conn +func StringWriteChan(conn net.Conn) chan string { + ch := make(chan string) + writer := bufio.NewWriter(conn) go func() { for str := range ch { - if _, err := rw.WriteString(str + "\r\n"); err != nil { - log.Print("StringChan[write]: %v", err) + if _, err := writer.WriteString(str + "\r\n"); err != nil { + log.Print("StringWriteChan[write]: ", err) break } - rw.Flush() + writer.Flush() } - done <- true + close(ch) }() return ch diff --git a/src/irc/protocol.go b/src/irc/protocol.go index f5699040..071601be 100644 --- a/src/irc/protocol.go +++ b/src/irc/protocol.go @@ -1,43 +1,107 @@ package irc import ( + "fmt" ) -type Message struct { - line string - client *Client +const ( + VERSION = "goircd-1" +) + +const ( + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_NONE = "300" +) + +func ReplyWelcome(nick string, user string, host string) string { + return fmt.Sprintf("%s %s Welcome to the Internet Relay Network %s!%s@%s", RPL_WELCOME, nick, nick, user, host) } -func (m *Message) Encode() string { - return m.line +func ReplyYourHost(nick string, server string) string { + return fmt.Sprintf("%s %s Your host is %s, running version %s", RPL_YOURHOST, nick, server, VERSION) +} + +func ReplyCreated(nick string, created string) string { + return fmt.Sprintf("%s %s This server was created %s", RPL_CREATED, nick, created) +} + +func ReplyMyInfo(nick string, servername string) string { + return fmt.Sprintf("%s %s %s %s ", RPL_MYINFO, nick, servername, VERSION) +} + +const ( + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_UNKNOWNCOMMAND = "421" + ERR_NICKNAMEINUSE = "433" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTRED = "462" + ERR_USERSDONTMATCH = "502" +) + +func ErrAlreadyRegistered(nick string) string { + return fmt.Sprintf("%s %s :You may not reregister", ERR_ALREADYREGISTRED, nick) +} + +func ErrNickNameInUse(nick string) string { + return fmt.Sprintf("%s %s :Nickname is already in use", ERR_NICKNAMEINUSE, nick) +} + +func ErrUnknownCommand(nick string, command string) string { + return fmt.Sprintf("%s %s %s :Unknown command", ERR_UNKNOWNCOMMAND, nick, command) } -// Adapt `chan string` to a `chan Message`. -func NewMessageChan(strch chan string) chan Message { - msgch := make(chan Message) +const ( + RE_PASS = "(?P\\S+)" + RE_NICK = "(?P\\S+)" + RE_USER = "(?P\\S+) (?P\\d) (?:\\S+) :(?P.+)" + RE_OPER = "(?P\\S+) (?P\\S+)" + RE_MODE = "(?P\\S+)(?: (?P[-+][iwroOs]+))*" + RE_SERVICE = "(?P\\S+) (?P\\S+) (?P\\S+) (?P\\S+) (?P\\S+) :(?P.+)" + RE_QUIT = "(?P.*)" + RE_SQUIT = "(?P\\S+) :(?P.+)" + RE_JOIN = "0|(?:(?P\\S+(?:,\\S+)*)(?: (?P\\S+(?:,\\S+)*))?)" + RE_PART = "(?P\\S+(?:,\\S+)*)(?: :(?P.+))?" + RE_MODE_CH = "(?P\\S+)(?: (?P[-+][iwroOs]+))*" // XXX incomplete + RE_TOPIC = "(?P\\S+)(?: :(?P.+))?" + RE_NAMES = "(?:(?P\\S+(?:,\\S+)*)(?: (?P\\S+))?)?" + RE_LIST = "(?:(?P\\S+(?:,\\S+)*)(?: (?P\\S+))?)?" + RE_INVITE = "(?P\\S+) (?P\\S+)" + RE_KICK = "(?P\\S+(?:,\\S+)*) (?P\\S+(?:,\\S+))(?: :(?P.+))?" + RE_PRIVMSG = "(?P\\S+) :(?P.+)" + RE_NOTICE = "(?P\\S+) :(?P.+)" + RE_MOTD = "(?P\\S+)?" + RE_LUSERS = "(?:(?P\\S+)(?: (?P\\S+))?)?" + RE_VERSION = "(?P\\S+)?" + RE_STATS = "(?:(?P\\S+)(?: (?P\\S+))?)?" + RE_LINKS = "(?:(?P\\S+) )?(?P\\S+)" + RE_TIME = "(?P\\S+)?" + RE_CONNECT = "(?P\\S+) (?P\\d+)(?: (?P\\S+))?" + RE_TRACE = "(?P\\S+)?" + RE_ADMIN = "(?P\\S+)?" + RE_INFO = "(?P\\S+)?" + RE_SERVLIST = "" // XXX + RE_SQUERY = "" // XXX + RE_WHO = "" // XXX + RE_WHOIS = "" // XXX + RE_WHOWAS = "" // XXX + RE_KILL = "(?P\\S+) :(?P.+)" + RE_PING = "(?P\\S+)(?: (?P\\S+))?" + RE_PONG = "(?P\\S+)(?: (?P\\S+))?" + RE_ERROR = ":(?P.+)" + RE_AWAY = ":(?P.+)" + RE_REHASH = "" + RE_DIE = "" + RE_RESTART = "" + RE_SUMMON = "(?P\\S+)(?: (?P\\S+)(?: (?P\\S+))?)?" + RE_USERS = "(?P\\S+)?" + RE_WALLOPS = ":(?P.+)" + RE_USERHOST = "(?P\\S+(?: \\S+)*)" + RE_ISON = "(?P\\S+(?: \\S+)*)" +) - done := make(chan bool) - go func() { - <- done - close(msgch) - }() - - // str -> msg - go func() { - for str := range strch { - msgch <- Message{str, nil} - } - done <- true - }() - - // msg -> str - go func() { - for message := range msgch { - strch <- message.Encode() - } - done <- true - }() - - return msgch -} diff --git a/src/irc/server.go b/src/irc/server.go index 39ca7bba..2085f647 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -3,14 +3,17 @@ package irc import ( "log" "net" + "strings" ) type Server struct { ch chan Message + users map[string]*Client + nicks map[string]*Client } func NewServer() *Server { - server := Server{make(chan Message)} + server := Server{make(chan Message), make(map[string]*Client), make(map[string]*Client)} go server.Receive() return &server } @@ -18,27 +21,69 @@ func NewServer() *Server { func (s *Server) Listen(addr string) { listener, err := net.Listen("tcp", addr) if err != nil { - log.Fatal("Server.Listen: %v", err) + log.Fatal("Server.Listen: ", err) } for { conn, err := listener.Accept() if err != nil { - log.Print("Server.Listen: %v", err) + log.Print("Server.Listen: ", err) continue } - client := NewClient(conn) - go client.Communicate(s.ch) + go NewClient(conn).Communicate(s) } } func (s *Server) Receive() { for message := range s.ch { - log.Print("Server.Receive: %v", message.line) - message.client.ch <- Message{"pong: " + message.line, nil} + log.Printf("C -> S: %s %s", message.command, message.args) + switch message.command { + case "PING": + message.client.Send("PONG") + case "PASS": + s.PassCommand(message.client, message.args) + case "USER": + s.UserCommand(message.client, message.args) + case "NICK": + s.NickCommand(message.client, message.args) + default: + message.client.Send(ErrUnknownCommand(message.client.nick, message.command)) + } } } -func (s *Server) Close() { - close(s.ch) +func (s *Server) Send(m Message) { + s.ch <- m +} + +// commands + +func (s *Server) PassCommand(c *Client, args string) { +} + +func (s *Server) UserCommand(c *Client, args string) { + parts := strings.SplitN(args, " ", 4) + username, _, _, realname := parts[0], parts[1], parts[2], parts[3] + if s.users[username] != nil { + c.Send(ErrAlreadyRegistered(c.nick)) + return + } + c.username, c.realname = username, realname + s.users[username] = c + if c.nick != "" { + c.Send( + ReplyWelcome(c.nick, c.username, "localhost"), + ReplyYourHost(c.nick, "irc.jlatt.com"), + ReplyCreated(c.nick, "2012/04/07"), + ReplyMyInfo(c.nick, "irc.jlatt.com")) + } +} + +func (s *Server) NickCommand(c *Client, nick string) { + if s.nicks[nick] != nil { + c.Send(ErrNickNameInUse(nick)) + return + } + c.nick = nick + s.nicks[nick] = c }