Base IRC server started.

Support for NICK, USER, QUIT, PING, and PONG commands added
This commit is contained in:
justin 2015-08-01 13:32:41 -04:00
parent 89428e0e2e
commit acfc3f2c60
4 changed files with 467 additions and 0 deletions

170
client.go Normal file
View File

@ -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
}
}

127
commands.go Normal file
View File

@ -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)
}
}

34
mux.go Normal file
View File

@ -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)
}

136
server.go Normal file
View File

@ -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())
}()
}
}