diff --git a/client.go b/client.go new file mode 100644 index 0000000..4669fcb --- /dev/null +++ b/client.go @@ -0,0 +1,170 @@ +package irc + +import ( + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/sorcix/irc" +) + +// Client represents an IRC Client connection to the server +type Client struct { + *irc.Conn + conn net.Conn + Nickname string + Name string + Host string + Username string + RealName string + + Prefix *irc.Prefix + + server *Server + authorized bool + + idleTimer *time.Timer + quitTimer *time.Timer + + awayMessage string +} + +func (s *Server) newClient(ircConn *irc.Conn, conn net.Conn) *Client { + client := &Client{Conn: ircConn, conn: conn, server: s} + client.authorized = len(s.config.Password) == 0 + client.idleTimer = time.AfterFunc(time.Minute, client.idle) + return client +} + +// Close cleans up the IRC client and closes the connection +func (c *Client) Close() error { + c.server.RemoveClient(c) + c.server.RemoveClientNick(c) + + return c.Conn.Close() +} + +// Ping sends an IRC PING command to a client +func (c *Client) Ping() { + m := irc.Message{Command: irc.PING, Params: []string{"JuddBot"}, Trailing: "JuddBot"} + c.Encode(&m) +} + +// Pong sends an IRC PONG command to the client +func (c *Client) Pong() { + m := irc.Message{Command: irc.PONG, Params: []string{"JuddBot"}, Trailing: "JuddBot"} + c.Encode(&m) +} + +func (c *Client) handleIncoming() { + c.server.AddClient(c) + for { + message, err := c.Decode() + if err != nil || message == nil { + + _, closedError := err.(*net.OpError) + if err == io.EOF || err == io.ErrClosedPipe || closedError || strings.Contains(err.Error(), "use of closed network connection") { + return + } + println("Error decoding incoming message", err.Error()) + return + //continue + } + //println(message.String()) + + c.idleTimer.Stop() + c.idleTimer = time.AfterFunc(time.Minute, c.idle) + if c.quitTimer != nil { + c.quitTimer.Stop() + c.quitTimer = nil + } + + c.server.CommandsMux.ServeIRC(message, c) + + } + +} + +func (c *Client) idle() { + c.Ping() + c.quitTimer = time.AfterFunc(time.Minute, c.quit) +} + +func (c *Client) quit() { + c.Quit() +} + +// Quit sends the IRC Quit command and closes the connection +func (c *Client) Quit() { + m := irc.Message{Prefix: &irc.Prefix{Name: c.server.config.Name}, Command: irc.QUIT, + Params: []string{c.Nickname}} + + c.Encode(&m) + c.Close() +} + +// Welcome handles initial client connection IRC protocols for a client. +// Welcome procedure includes IRC WELCOME, Host Info, and MOTD +func (c *Client) Welcome() { + + // Have all client info now + c.Prefix = &irc.Prefix{Name: c.Nickname, User: c.Username, Host: c.Host} + + m := irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_WELCOME, + Params: []string{c.Nickname, c.server.config.Welcome}} + + err := c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_YOURHOST, + Params: []string{c.Nickname, fmt.Sprintf("Your host is %s", c.server.config.Name)}} + + err = c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_CREATED, + Params: []string{c.Nickname, fmt.Sprintf("This server was created %s", c.server.created)}} + + err = c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_MYINFO, + Params: []string{c.Nickname, fmt.Sprintf("%s - Golang IRC server", c.server.config.Name)}} + + err = c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_MOTDSTART, + Params: []string{c.Nickname, fmt.Sprintf("%s - Message of the day", c.server.config.Name)}} + + err = c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_MOTD, + Params: []string{c.Nickname, c.server.config.MOTD}} + + err = c.Encode(&m) + if err != nil { + return + } + + m = irc.Message{Prefix: c.server.Prefix, Command: irc.RPL_ENDOFMOTD, + Params: []string{c.Nickname, "End of MOTD"}} + + err = c.Encode(&m) + if err != nil { + return + } +} diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..74f3084 --- /dev/null +++ b/commands.go @@ -0,0 +1,127 @@ +package irc + +import ( + "fmt" + + "github.com/sorcix/irc" +) + +// CommandHandler allows objects implementing this interface to be registered to serve a particular IRC command +type CommandHandler interface { + ServeIRC(message *irc.Message, client *Client) +} + +// CommandHandlerFunc is a wrapper to to use regular functions as a CommandHandler +type CommandHandlerFunc func(message *irc.Message, client *Client) + +// ServeIRC services a given IRC message from the given client +func (f CommandHandlerFunc) ServeIRC(message *irc.Message, client *Client) { + f(message, client) +} + +// PingHandler is a CommandHandler to respond to IRC PING commands from a client +// Implemented according to RFC 1459 4.6.2 and RFC 2812 3.7.2 +func PingHandler(message *irc.Message, client *Client) { + client.Pong() + +} + +// PongHandler is a CommandHandler to respond to IRC PONG commands from a client +// Implemented according to RFC 1459 4.6.3 and RFC 2812 3.7.3 +func PongHandler(message *irc.Message, client *Client) { + //client.Ping() + +} + +// QuitHandler is a CommandHandler to respond to IRC QUIT commands from a client +// Implemented according to RFC 1459 4.1.6 and RFC 2812 3.1.7 +func QuitHandler(message *irc.Message, client *Client) { + + m := irc.Message{Prefix: client.server.Prefix, Command: irc.ERROR, Trailing: "quit"} + + client.Encode(&m) + client.Close() +} + +// NickHandler is a CommandHandler to respond to IRC NICK commands from a client +// Implemented according to RFC 1459 4.1.2 and RFC 2812 3.1.2 +func NickHandler(message *irc.Message, client *Client) { + + var m irc.Message + name := client.server.config.Name + nickname := client.Nickname + + if len(message.Params) == 0 { + m = irc.Message{Prefix: &irc.Prefix{Name: name}, Command: irc.ERR_NONICKNAMEGIVEN, Trailing: "No nickname given"} + client.Encode(&m) + return + } + + newNickname := message.Params[0] + + _, found := client.server.ClientsByNick[newNickname] + + switch { + case !client.authorized: + m = irc.Message{Prefix: &irc.Prefix{Name: name}, Command: irc.ERR_PASSWDMISMATCH, Params: []string{newNickname}, Trailing: "Password incorrect"} + + case found: // nickname already in use + fmt.Println("Nickname already used") + m = irc.Message{Prefix: &irc.Prefix{Name: name}, Command: irc.ERR_NICKNAMEINUSE, Params: []string{newNickname}, Trailing: "Nickname is already in use"} + + default: + if len(client.Nickname) == 0 && len(client.Username) != 0 { // Client is connected now, show MOTD ... + client.Nickname = newNickname + client.server.AddClientNick(client) + client.Welcome() + } else { //change client name + client.Nickname = newNickname + client.server.UpdateClientNick(client, nickname) + //fmt.Println("Updating client name") + } + } + + if len(m.Command) != 0 { + client.Encode(&m) + } + +} + +// UserHandler is a CommandHandler to respond to IRC USER commands from a client +// Implemented according to RFC 1459 4.1.3 and RFC 2812 3.1.3 +func UserHandler(message *irc.Message, client *Client) { + var m irc.Message + serverName := client.server.config.Name + //nickname := client.Nickname + + if len(client.Username) != 0 { // Already registered + m = irc.Message{Prefix: &irc.Prefix{Name: serverName}, Command: irc.ERR_ALREADYREGISTRED, Trailing: "You may not reregister"} + client.Encode(&m) + return + } + + if len(message.Params) != 3 { + m = irc.Message{Prefix: &irc.Prefix{Name: serverName}, Command: irc.ERR_NEEDMOREPARAMS, Trailing: "Not enough parameters"} + client.Encode(&m) + return + } + + name := message.Params[0] + username := message.Params[1] + hostname := message.Params[2] + realName := message.Trailing + + client.Name = name + client.Username = username + client.Host = hostname + client.RealName = realName + if len(m.Command) == 0 && len(client.Nickname) != 0 { // Client has finished connecting + client.Welcome() + return + } + + if len(m.Command) != 0 { + client.Encode(&m) + } + +} diff --git a/mux.go b/mux.go new file mode 100644 index 0000000..afeb331 --- /dev/null +++ b/mux.go @@ -0,0 +1,34 @@ +package irc + +import "github.com/sorcix/irc" + +// CommandsMux multiplexes incoming IRC commands +type CommandsMux struct { + commands map[string]CommandHandler +} + +// NewCommandsMux creates and returns a new CommandsMux +func NewCommandsMux() CommandsMux { + return CommandsMux{commands: map[string]CommandHandler{}} +} + +// Handle registers the given CommandHandler for a given IRC command +func (c *CommandsMux) Handle(command string, handler CommandHandler) { + c.commands[command] = handler +} + +// HandleFunc registers the given handler function for a given IRC command +func (c *CommandsMux) HandleFunc(command string, handler CommandHandlerFunc) { + c.commands[command] = CommandHandler(handler) +} + +// ServeIRC dispatches the incoming IRC command to the appropriate handler +func (c *CommandsMux) ServeIRC(message *irc.Message, client *Client) { + h, ok := c.commands[message.Command] + if !ok { + m := irc.Message{Command: irc.ERR_UNKNOWNCOMMAND} + client.Encode(&m) + return + } + h.ServeIRC(message, client) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..dc17dc8 --- /dev/null +++ b/server.go @@ -0,0 +1,136 @@ +package irc + +import ( + "crypto/tls" + "fmt" + "net" + "sync" + "time" + + "github.com/sorcix/irc" +) + +// Server represents an IRC server +type Server struct { + config ServerConfig + + Clients map[net.Addr]*Client + clientMutex sync.RWMutex + + ClientsByNick map[string]*Client + clientByNickMutex sync.RWMutex + + Prefix *irc.Prefix + CommandsMux CommandsMux + created time.Time +} + +// ServerConfig contains configuration data for seeding a server +type ServerConfig struct { + Name string + MOTD string + Welcome string + TLSConfig *tls.Config + Addr string + + Password string +} + +// NewServer creates and returns a new Server based on the provided config +func NewServer(config ServerConfig) *Server { + s := Server{} + s.config = config + s.Clients = map[net.Addr]*Client{} + s.CommandsMux = NewCommandsMux() + s.created = time.Now() + s.ClientsByNick = map[string]*Client{} + s.Prefix = &irc.Prefix{Name: config.Name} + + return &s +} + +// AddClient adds a new Client +func (s *Server) AddClient(client *Client) { + s.clientMutex.Lock() + defer s.clientMutex.Unlock() + s.Clients[client.conn.RemoteAddr()] = client +} + +// RemoveClient removes a client +func (s *Server) RemoveClient(client *Client) { + s.clientMutex.Lock() + defer s.clientMutex.Unlock() + delete(s.Clients, client.conn.RemoteAddr()) +} + +// GetClient finds a client by its address and returns it +func (s *Server) GetClient(addr net.Addr) *Client { + s.clientMutex.RLock() + defer s.clientMutex.RUnlock() + return s.Clients[addr] +} + +// AddClientNick adds a client based on its nickname +func (s *Server) AddClientNick(client *Client) { + s.clientByNickMutex.Lock() + defer s.clientByNickMutex.Unlock() + s.ClientsByNick[client.Nickname] = client +} + +// RemoveClientNick removes a client based on its nickname +func (s *Server) RemoveClientNick(client *Client) { + s.clientByNickMutex.Lock() + defer s.clientByNickMutex.Unlock() + delete(s.ClientsByNick, client.Nickname) +} + +// UpdateClientNick updates the nickname of a client as it is stored by the server +func (s *Server) UpdateClientNick(client *Client, oldNick string) { + s.clientByNickMutex.Lock() + defer s.clientByNickMutex.Unlock() + delete(s.ClientsByNick, oldNick) + s.ClientsByNick[client.Nickname] = client +} + +// GetClientByNick returns a client with the corresponding nickname +func (s *Server) GetClientByNick(nick string) *Client { + s.clientByNickMutex.RLock() + defer s.clientByNickMutex.RUnlock() + return s.ClientsByNick[nick] +} + +// Start the server listening on the configured port +func (s *Server) Start() { + var listener net.Listener + var err error + if s.config.TLSConfig != nil { + listener, err = tls.Listen("tcp", s.config.Addr, s.config.TLSConfig) + } else { + listener, err = net.Listen("tcp", s.config.Addr) + } + + if err != nil { + fmt.Println("Error starting listner", err.Error()) + return + } + + for { + conn, err := listener.Accept() + if err != nil { + fmt.Println("Error accepting connection", err.Error()) + //return + continue + } + + ircConn := irc.NewConn(conn) + client := s.newClient(ircConn, conn) + + defer client.Close() + go func() { + fmt.Println("Incoming connection from:", conn.RemoteAddr()) + client.handleIncoming() + fmt.Println("Disconnected with:", conn.RemoteAddr()) + }() + + } +}