diff --git a/.gitignore b/.gitignore index 5a7432d2..ac4d22c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ pkg bin -ergonomadic +src/code.google.com/ diff --git a/build.sh b/build.sh index f03e1bb9..725dde50 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,4 @@ #!/bin/bash -env GOPATH="$PWD" go install -v ergonomadic +export GOPATH="$PWD" +go get "code.google.com/p/go.crypto/bcrypt" +go install -v ergonomadic genpasswd diff --git a/src/ergonomadic/ergonomadic.go b/src/ergonomadic/ergonomadic.go index 3567ea20..46e108fa 100644 --- a/src/ergonomadic/ergonomadic.go +++ b/src/ergonomadic/ergonomadic.go @@ -8,6 +8,6 @@ import ( func main() { name := flag.String("name", "localhost", "A name for the server") listen := flag.String("listen", ":6667", "interface to listen on") - flag.Parse() + flag.Parse() irc.NewServer(*name).Listen(*listen) } diff --git a/src/genpasswd/genpasswd.go b/src/genpasswd/genpasswd.go new file mode 100644 index 00000000..093bf021 --- /dev/null +++ b/src/genpasswd/genpasswd.go @@ -0,0 +1,18 @@ +package main + +import ( + "code.google.com/p/go.crypto/bcrypt" + "encoding/base64" + "flag" + "fmt" +) + +func main() { + flag.Parse() + password := flag.Arg(0) + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + fmt.Println(base64.StdEncoding.EncodeToString(hash)) +} diff --git a/src/irc/channel.go b/src/irc/channel.go index 83037c18..cb691b6d 100644 --- a/src/irc/channel.go +++ b/src/irc/channel.go @@ -2,33 +2,82 @@ package irc type Channel struct { server *Server + replies chan<- Reply + commands chan<- ChannelCommand name string key string topic string - members ClientSet + members UserSet noOutside bool password string } type ChannelSet map[*Channel]bool +func (set ChannelSet) Add(channel *Channel) { + set[channel] = true +} + +func (set ChannelSet) Remove(channel *Channel) { + delete(set, channel) +} + +type ChannelCommand interface { + Command + HandleChannel(channel *Channel) +} + +type JoinChannelCommand struct { + *JoinCommand + key string +} + +type PartChannelCommand struct { + Command + message string +} + +type GetTopicChannelCommand struct { + *TopicCommand +} + +type SetTopicChannelCommand struct { + *TopicCommand +} + +type PrivMsgChannelCommand struct { + *PrivMsgCommand +} + // NewChannel creates a new channel from a `Server` and a `name` string, which // must be unique on the server. func NewChannel(s *Server, name string) *Channel { - return &Channel{ - name: name, - members: make(ClientSet), - server: s, + replies := make(chan Reply) + commands := make(chan ChannelCommand) + channel := &Channel{ + name: name, + members: make(UserSet), + server: s, + commands: commands, + replies: replies, + } + go channel.receiveReplies(replies) + go channel.receiveCommands(commands) + return channel +} + +// Forward `Reply`s to all `User`s of the `Channel`. +func (ch *Channel) receiveReplies(replies <-chan Reply) { + for reply := range replies { + for client := range ch.members { + client.replies <- reply + } } } -// Send a `Reply` to all `Client`s of the `Channel`. Skip `fromClient`, if it is -// provided. -func (ch *Channel) Send(reply Reply, fromClient *Client) { - for client := range ch.members { - if client != fromClient { - client.send <- reply - } +func (ch *Channel) receiveCommands(commands <-chan ChannelCommand) { + for command := range commands { + command.HandleChannel(ch) } } @@ -47,72 +96,86 @@ func (ch *Channel) IsEmpty() bool { } // -// channel functionality +// commands // -func (ch *Channel) Join(cl *Client, key string) { - if ch.key != key { - cl.send <- ErrBadChannelKey(ch) +func (m *JoinChannelCommand) HandleChannel(channel *Channel) { + client := m.Client() + user := client.user + + if channel.key != m.key { + client.user.replies <- ErrBadChannelKey(channel) return } - ch.members[cl] = true - cl.channels[ch] = true + channel.members.Add(client.user) + client.user.channels.Add(channel) - ch.Send(RplJoin(ch, cl), nil) - ch.GetTopic(cl) - cl.send <- RplNamReply(ch) - cl.send <- RplEndOfNames(ch.server) + channel.replies <- RplJoin(channel, user) + channel.GetTopic(user) + client.user.replies <- RplNamReply(channel) + client.user.replies <- RplEndOfNames(channel.server) } -func (ch *Channel) Part(cl *Client, message string) { - if !ch.members[cl] { - cl.send <- ErrNotOnChannel(ch) +func (m *PartChannelCommand) HandleChannel(channel *Channel) { + user := m.Client().user + + if !channel.members[user] { + user.replies <- ErrNotOnChannel(channel) return } - if message == "" { - message = cl.Nick() + msg := m.message + if msg == "" { + msg = user.Nick() } - ch.Send(RplPart(ch, cl, message), nil) + channel.replies <- RplPart(channel, user, msg) - delete(ch.members, cl) - delete(cl.channels, ch) + channel.members.Remove(user) + user.channels.Remove(channel) - if len(ch.members) == 0 { - ch.server.DeleteChannel(ch) + if len(channel.members) == 0 { + channel.server.DeleteChannel(channel) } } -func (ch *Channel) PrivMsg(cl *Client, message string) { - ch.Send(RplPrivMsgChannel(ch, cl, message), cl) -} - -func (ch *Channel) GetTopic(cl *Client) { - if !ch.members[cl] { - cl.send <- ErrNotOnChannel(ch) +func (channel *Channel) GetTopic(user *User) { + if !channel.members[user] { + user.replies <- ErrNotOnChannel(channel) return } - if ch.topic != "" { - cl.send <- RplTopic(ch) - } else { - cl.send <- RplNoTopic(ch) - } -} - -func (ch *Channel) ChangeTopic(cl *Client, newTopic string) { - if !ch.members[cl] { - cl.send <- ErrNotOnChannel(ch) + if channel.topic == "" { + user.replies <- RplNoTopic(channel) return } - ch.topic = newTopic - - if ch.topic != "" { - ch.Send(RplTopic(ch), nil) - } else { - ch.Send(RplNoTopic(ch), nil) - } + user.replies <- RplTopic(channel) +} + +func (m *GetTopicChannelCommand) HandleChannel(channel *Channel) { + channel.GetTopic(m.Client().user) +} + +func (m *SetTopicChannelCommand) HandleChannel(channel *Channel) { + user := m.Client().user + + if !channel.members[user] { + user.replies <- ErrNotOnChannel(channel) + return + } + + channel.topic = m.topic + + if channel.topic == "" { + channel.replies <- RplNoTopic(channel) + return + } + + channel.replies <- RplTopic(channel) +} + +func (m *PrivMsgChannelCommand) HandleChannel(channel *Channel) { + channel.replies <- RplPrivMsgChannel(channel, m.Client().user, m.message) } diff --git a/src/irc/client.go b/src/irc/client.go index 0a5b22e3..37ee72cb 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -9,7 +9,7 @@ import ( type Client struct { conn net.Conn - send chan<- Reply + replies chan<- Reply username string realname string hostname string @@ -17,10 +17,9 @@ type Client struct { serverPass bool registered bool away bool - wallOps bool server *Server - channels ChannelSet atime time.Time + user *User } type ClientSet map[*Client]bool @@ -28,21 +27,20 @@ type ClientSet map[*Client]bool func NewClient(server *Server, conn net.Conn) *Client { read := StringReadChan(conn) write := StringWriteChan(conn) - send := make(chan Reply) + replies := make(chan Reply) client := &Client{ - channels: make(ChannelSet), conn: conn, hostname: LookupHostname(conn.RemoteAddr()), server: server, - send: send, + replies: replies, } // Connect the conn to the server. go client.readConn(read) // Connect the reply channel to the conn. - go client.writeConn(write, send) + go client.writeConn(write, replies) return client } @@ -51,19 +49,19 @@ func (c *Client) readConn(recv <-chan string) { for str := range recv { log.Printf("%s > %s", c.Id(), str) - m, err := ParseMessage(str) + m, err := ParseCommand(str) if err != nil { // TODO handle error continue } m.SetClient(c) - c.server.recv <- m + c.server.commands <- m } } -func (c *Client) writeConn(write chan<- string, send <-chan Reply) { - for reply := range send { +func (c *Client) writeConn(write chan<- string, replies <-chan Reply) { + for reply := range replies { replyStr := reply.String(c) log.Printf("%s < %s", c.Id(), replyStr) write <- replyStr @@ -71,16 +69,18 @@ func (c *Client) writeConn(write chan<- string, send <-chan Reply) { } func (c *Client) Nick() string { + if c.user != nil { + return c.user.nick + } + if c.nick != "" { return c.nick } + return "*" } func (c *Client) UModeString() string { - if c.wallOps { - return "+w" - } return "" } @@ -106,3 +106,7 @@ func (c *Client) UserHost() string { func (c *Client) Id() string { return c.UserHost() } + +func (c *Client) PublicId() string { + return fmt.Sprintf("%s!%s@%s", c.Nick(), c.Nick(), c.server.Id()) +} diff --git a/src/irc/commands.go b/src/irc/commands.go index 7cf48800..164ff34f 100644 --- a/src/irc/commands.go +++ b/src/irc/commands.go @@ -2,63 +2,66 @@ package irc import ( "errors" - "fmt" - "regexp" "strconv" "strings" ) -type Message interface { - Handle(s *Server, c *Client) +type Command interface { Client() *Client - SetClient(c *Client) + SetClient(*Client) + Handle(*Server) } var ( - NotEnoughArgsError = errors.New("not enough arguments") - UModeUnknownFlagError = errors.New("unknown umode flag") + NotEnoughArgsError = errors.New("not enough arguments") ) -type BaseMessage struct { +type BaseCommand struct { client *Client } -func (m *BaseMessage) Client() *Client { - return m.client +func (base *BaseCommand) Client() *Client { + return base.client } -func (m *BaseMessage) SetClient(c *Client) { - m.client = c +func (base *BaseCommand) SetClient(c *Client) { + base.client = c } // unknown [args...] -type UnknownMessage struct { - *BaseMessage +type UnknownCommand struct { + *BaseCommand command string args []string } -// NB: no constructor, created on demand in parser for invalid messages. +func NewUnknownCommand(command string, args []string) Command { + return &UnknownCommand{ + BaseCommand: &BaseCommand{}, + command: command, + args: args, + } +} -func (m *UnknownMessage) Handle(s *Server, c *Client) { - c.send <- ErrUnknownCommand(s, m.command) +func (m *UnknownCommand) Handle(s *Server) { + m.Client().replies <- ErrUnknownCommand(s, m.command) } // PING [ ] -type PingMessage struct { - *BaseMessage +type PingCommand struct { + *BaseCommand server string server2 string } -func NewPingMessage(args []string) (Message, error) { +func NewPingCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &PingMessage{ - BaseMessage: &BaseMessage{}, + msg := &PingCommand{ + BaseCommand: &BaseCommand{}, server: args[0], } if len(args) > 1 { @@ -67,95 +70,78 @@ func NewPingMessage(args []string) (Message, error) { return msg, nil } -func (m *PingMessage) Handle(s *Server, c *Client) { - c.send <- RplPong(s) -} - // PONG [ ] -type PongMessage struct { - *BaseMessage +type PongCommand struct { + *BaseCommand server1 string server2 string } -func NewPongMessage(args []string) (Message, error) { +func NewPongCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - message := &PongMessage{server1: args[0]} + message := &PongCommand{ + BaseCommand: &BaseCommand{}, + server1: args[0], + } if len(args) > 1 { message.server2 = args[1] } return message, nil } -func (m *PongMessage) Handle(s *Server, c *Client) { - // no-op -} - // PASS -type PassMessage struct { - *BaseMessage +type PassCommand struct { + *BaseCommand password string } -func NewPassMessage(args []string) (Message, error) { +func NewPassCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - return &PassMessage{ - BaseMessage: &BaseMessage{}, + return &PassCommand{ + BaseCommand: &BaseCommand{}, password: args[0], }, nil } -func (m *PassMessage) Handle(s *Server, c *Client) { - if m.password == s.password { - c.serverPass = true - } else { - c.send <- ErrPasswdMismatch(s) - } -} - // NICK -type NickMessage struct { - *BaseMessage +type NickCommand struct { + *BaseCommand nickname string } -func NewNickMessage(args []string) (Message, error) { +func NewNickCommand(args []string) (Command, error) { if len(args) != 1 { return nil, NotEnoughArgsError } - return &NickMessage{ - BaseMessage: &BaseMessage{}, + return &NickCommand{ + BaseCommand: &BaseCommand{}, nickname: args[0], }, nil } -func (m *NickMessage) Handle(s *Server, c *Client) { - s.ChangeNick(c, m.nickname) -} - // USER -type UserMessage struct { - *BaseMessage +type UserCommand struct { + *BaseCommand user string mode uint8 unused string realname string } -func NewUserMessage(args []string) (Message, error) { +func NewUserCommand(args []string) (Command, error) { if len(args) != 4 { return nil, NotEnoughArgsError } - msg := &UserMessage{ - BaseMessage: &BaseMessage{}, + msg := &UserCommand{ + BaseCommand: &BaseCommand{}, user: args[0], unused: args[2], realname: args[3], @@ -167,20 +153,16 @@ func NewUserMessage(args []string) (Message, error) { return msg, nil } -func (m *UserMessage) Handle(s *Server, c *Client) { - s.UserLogin(c, m.user, m.realname) -} +// QUIT [ ] -// QUIT [ ] - -type QuitMessage struct { - *BaseMessage +type QuitCommand struct { + *BaseCommand message string } -func NewQuitMessage(args []string) (Message, error) { - msg := &QuitMessage{ - BaseMessage: &BaseMessage{}, +func NewQuitCommand(args []string) (Command, error) { + msg := &QuitCommand{ + BaseCommand: &BaseCommand{}, } if len(args) > 0 { msg.message = args[0] @@ -188,98 +170,18 @@ func NewQuitMessage(args []string) (Message, error) { return msg, nil } -func (m *QuitMessage) Handle(s *Server, c *Client) { - s.Quit(c, m.message) -} - -// MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) - -type ModeMessage struct { - *BaseMessage - nickname string - modes []string -} - -type ChannelModeMessage struct { - *ModeMessage - channel string - modeParams []string -} - -// mode s is accepted but ignored, like some other modes -var MODE_RE = regexp.MustCompile("^[-+][iwroOs]+$") -var CHANNEL_RE = regexp.MustCompile("^[+\\&\\!#][:alnum:]+$") -var EXTRACT_MODE_RE = regexp.MustCompile("^([-+])?([aimnqpsrtklbeI]+)$") - -func NewModeMessage(args []string) (Message, error) { - if len(args) < 1 { - return nil, NotEnoughArgsError - } - - if (len(args) > 1) && CHANNEL_RE.MatchString(args[1]) { - cmsg := new(ChannelModeMessage) - cmsg.nickname = args[0] - if len(args) > 2 { - groups := EXTRACT_MODE_RE.FindStringSubmatch(args[2]) - cmsg.modes = make([]string, len(groups[2])) - i := 0 - for _, char := range groups[2] { - cmsg.modes[i] = fmt.Sprintf("%s%c", groups[1], char) - i++ - } - } - if len(args) > 3 { - cmsg.modeParams = strings.Split(args[3], ",") - } - return cmsg, nil - } - - msg := &ModeMessage{ - BaseMessage: &BaseMessage{}, - nickname: args[0], - } - for _, arg := range args[1:] { - if !MODE_RE.MatchString(arg) { - return nil, UModeUnknownFlagError - } - prefix := arg[0] - for _, c := range arg[1:] { - mode := fmt.Sprintf("%c%c", prefix, c) - msg.modes = append(msg.modes, mode) - } - } - return msg, nil -} - -func (m *ModeMessage) Handle(s *Server, c *Client) { - if m.nickname != c.nick { - c.send <- ErrUsersDontMatch(s) - return - } - s.ChangeUserMode(c, m.modes) -} - -func (m *ChannelModeMessage) Handle(s *Server, c *Client) { - channel := s.channels[m.channel] - if channel != nil { - c.send <- ErrNoChanModes(channel) - } else { - c.send <- ErrNoSuchChannel(s, m.channel) - } -} - // JOIN ( *( "," ) [ *( "," ) ] ) / "0" -type JoinMessage struct { - *BaseMessage +type JoinCommand struct { + *BaseCommand channels []string keys []string zero bool } -func NewJoinMessage(args []string) (Message, error) { - msg := &JoinMessage{ - BaseMessage: &BaseMessage{}, +func NewJoinCommand(args []string) (Command, error) { + msg := &JoinCommand{ + BaseCommand: &BaseCommand{}, } if len(args) > 0 { if args[0] == "0" { @@ -295,38 +197,20 @@ func NewJoinMessage(args []string) (Message, error) { return msg, nil } -func (m *JoinMessage) Handle(s *Server, c *Client) { - if m.zero { - for channel := range c.channels { - channel.Part(c, "") - } - } else { - for i, name := range m.channels { - key := "" - if len(m.keys) > i { - key = m.keys[i] - } +// PART *( "," ) [ ] - s.GetOrMakeChannel(name).Join(c, key) - } - } -} - -// PART *( "," ) [ ] - -type PartMessage struct { - *BaseMessage +type PartCommand struct { + *BaseCommand channels []string message string } -func NewPartMessage(args []string) (Message, error) { +func NewPartCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &PartMessage{ - - BaseMessage: &BaseMessage{}, + msg := &PartCommand{ + BaseCommand: &BaseCommand{}, channels: strings.Split(args[0], ","), } if len(args) > 1 { @@ -335,39 +219,26 @@ func NewPartMessage(args []string) (Message, error) { return msg, nil } -func (m *PartMessage) Handle(s *Server, c *Client) { - for _, chname := range m.channels { - channel := s.channels[chname] - - if channel == nil { - c.send <- ErrNoSuchChannel(s, chname) - continue - } - - channel.Part(c, m.message) - } -} - // PRIVMSG -type PrivMsgMessage struct { - *BaseMessage +type PrivMsgCommand struct { + *BaseCommand target string message string } -func NewPrivMsgMessage(args []string) (Message, error) { +func NewPrivMsgCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } - return &PrivMsgMessage{ - BaseMessage: &BaseMessage{}, + return &PrivMsgCommand{ + BaseCommand: &BaseCommand{}, target: args[0], message: args[1], }, nil } -func (m *PrivMsgMessage) TargetIsChannel() bool { +func (m *PrivMsgCommand) TargetIsChannel() bool { switch m.target[0] { case '&', '#', '+', '!': return true @@ -375,35 +246,20 @@ func (m *PrivMsgMessage) TargetIsChannel() bool { return false } -func (m *PrivMsgMessage) Handle(s *Server, c *Client) { - if m.TargetIsChannel() { - if channel := s.channels[m.target]; channel != nil { - channel.PrivMsg(c, m.message) - return - } - } else { - if client := s.nicks[m.target]; client != nil { - client.send <- RplPrivMsg(c, client, m.message) - return - } - } - c.send <- ErrNoSuchNick(s, m.target) -} - // TOPIC [newtopic] -type TopicMessage struct { - *BaseMessage +type TopicCommand struct { + *BaseCommand channel string topic string } -func NewTopicMessage(args []string) (Message, error) { +func NewTopicCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &TopicMessage{ - BaseMessage: &BaseMessage{}, + msg := &TopicCommand{ + BaseCommand: &BaseCommand{}, channel: args[0], } if len(args) > 1 { @@ -412,38 +268,40 @@ func NewTopicMessage(args []string) (Message, error) { return msg, nil } -func (m *TopicMessage) Handle(s *Server, c *Client) { - channel := s.channels[m.channel] - if channel == nil { - c.send <- ErrNoSuchChannel(s, m.channel) - return - } - if m.topic == "" { - channel.GetTopic(c) - } else { - channel.ChangeTopic(c, m.topic) - } -} - // LOGIN -type LoginMessage struct { - *BaseMessage +type LoginCommand struct { + *BaseCommand nick string password string } -func NewLoginMessage(args []string) (Message, error) { +func NewLoginCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } - return &LoginMessage{ - BaseMessage: &BaseMessage{}, + return &LoginCommand{ + BaseCommand: &BaseCommand{}, nick: args[0], password: args[1], }, nil } -func (m *LoginMessage) Handle(s *Server, c *Client) { - // TODO +// RESERVE + +type ReserveCommand struct { + *BaseCommand + nick string + password string +} + +func NewReserveCommand(args []string) (Command, error) { + if len(args) < 2 { + return nil, NotEnoughArgsError + } + return &ReserveCommand{ + BaseCommand: &BaseCommand{}, + nick: args[0], + password: args[1], + }, nil } diff --git a/src/irc/parse.go b/src/irc/parse.go index 61fe1049..b3f8f365 100644 --- a/src/irc/parse.go +++ b/src/irc/parse.go @@ -5,35 +5,30 @@ import ( "strings" ) -type ParseFunc func([]string) (Message, error) +type ParseFunc func([]string) (Command, error) var ( - ErrParseMessage = errors.New("failed to parse message") + ErrParseCommand = errors.New("failed to parse message") parseCommandFuncs = map[string]ParseFunc{ - "JOIN": NewJoinMessage, - "MODE": NewModeMessage, - "LOGIN": NewLoginMessage, - "NICK": NewNickMessage, - "PART": NewPartMessage, - "PASS": NewPassMessage, - "PING": NewPingMessage, - "PONG": NewPongMessage, - "PRIVMSG": NewPrivMsgMessage, - "QUIT": NewQuitMessage, - "TOPIC": NewTopicMessage, - "USER": NewUserMessage, + "JOIN": NewJoinCommand, + "LOGIN": NewLoginCommand, + "NICK": NewNickCommand, + "PART": NewPartCommand, + "PASS": NewPassCommand, + "PING": NewPingCommand, + "PONG": NewPongCommand, + "PRIVMSG": NewPrivMsgCommand, + "QUIT": NewQuitCommand, + "TOPIC": NewTopicCommand, + "USER": NewUserCommand, } ) -func ParseMessage(line string) (Message, error) { +func ParseCommand(line string) (Command, error) { command, args := parseLine(line) - constructor, ok := parseCommandFuncs[command] - if !ok { - return &UnknownMessage{ - BaseMessage: &BaseMessage{}, - command: command, - args: args, - }, nil + constructor := parseCommandFuncs[command] + if constructor == nil { + return NewUnknownCommand(command, args), nil } return constructor(args) } diff --git a/src/irc/reply.go b/src/irc/reply.go index 72a1e986..5a2e9f4e 100644 --- a/src/irc/reply.go +++ b/src/irc/reply.go @@ -8,6 +8,7 @@ import ( type Identifier interface { Id() string + PublicId() string } type Reply interface { @@ -20,7 +21,8 @@ type BasicReply struct { message string } -func NewBasicReply(source Identifier, code string, message string) *BasicReply { +func NewBasicReply(source Identifier, code string, format string, args ...interface{}) *BasicReply { + message := fmt.Sprintf(format, args...) fullMessage := fmt.Sprintf(":%s %s %s\r\n", source.Id(), code, message) return &BasicReply{source, code, fullMessage} } @@ -33,8 +35,8 @@ type NumericReply struct { *BasicReply } -func NewNumericReply(source Identifier, code string, message string) *NumericReply { - return &NumericReply{&BasicReply{source, code, message}} +func NewNumericReply(source Identifier, code string, format string, args ...interface{}) *NumericReply { + return &NumericReply{&BasicReply{source, code, fmt.Sprintf(format, args...)}} } func (reply *NumericReply) String(client *Client) string { @@ -44,24 +46,24 @@ func (reply *NumericReply) String(client *Client) string { // messaging replies -func RplPrivMsg(source *Client, target *Client, message string) Reply { - return NewBasicReply(source, RPL_PRIVMSG, fmt.Sprintf("%s :%s", target, message)) +func RplPrivMsg(source Identifier, target Identifier, message string) Reply { + return NewBasicReply(source, RPL_PRIVMSG, "%s :%s", target, message) } func RplNick(client *Client, newNick string) Reply { return NewBasicReply(client, RPL_NICK, newNick) } -func RplPrivMsgChannel(channel *Channel, source *Client, message string) Reply { - return NewBasicReply(source, RPL_PRIVMSG, fmt.Sprintf("%s :%s", channel.name, message)) +func RplPrivMsgChannel(channel *Channel, source Identifier, message string) Reply { + return NewBasicReply(source, RPL_PRIVMSG, "%s :%s", channel.name, message) } -func RplJoin(channel *Channel, client *Client) Reply { - return NewBasicReply(client, RPL_JOIN, channel.name) +func RplJoin(channel *Channel, user *User) Reply { + return NewBasicReply(user, RPL_JOIN, channel.name) } -func RplPart(channel *Channel, client *Client, message string) Reply { - return NewBasicReply(client, RPL_PART, fmt.Sprintf("%s :%s", channel.name, message)) +func RplPart(channel *Channel, user *User, message string) Reply { + return NewBasicReply(user, RPL_PART, "%s :%s", channel.name, message) } func RplPong(server *Server) Reply { @@ -69,7 +71,7 @@ func RplPong(server *Server) Reply { } func RplQuit(client *Client, message string) Reply { - return NewBasicReply(client, RPL_QUIT, ":"+message) + return NewBasicReply(client, RPL_QUIT, ":%", message) } func RplInviteMsg(channel *Channel, inviter *Client) Reply { @@ -80,70 +82,75 @@ func RplInviteMsg(channel *Channel, inviter *Client) Reply { func RplWelcome(source Identifier, client *Client) Reply { return NewNumericReply(source, RPL_WELCOME, - "Welcome to the Internet Relay Network "+client.Id()) + "Welcome to the Internet Relay Network %s", client.Id()) } func RplYourHost(server *Server, target *Client) Reply { return NewNumericReply(server, RPL_YOURHOST, - fmt.Sprintf("Your host is %s, running version %s", server.hostname, VERSION)) + "Your host is %s, running version %s", server.hostname, VERSION) } func RplCreated(server *Server) Reply { return NewNumericReply(server, RPL_CREATED, - "This server was created "+server.ctime.Format(time.RFC1123)) + "This server was created %s", server.ctime.Format(time.RFC1123)) } func RplMyInfo(server *Server) Reply { return NewNumericReply(server, RPL_MYINFO, - fmt.Sprintf("%s %s w kn", server.name, VERSION)) + "%s %s a kn", server.name, VERSION) } func RplUModeIs(server *Server, client *Client) Reply { - return NewNumericReply(server, RPL_UMODEIS, client.UModeString()) + return NewNumericReply(server, RPL_UMODEIS, + client.UModeString()) } func RplNoTopic(channel *Channel) Reply { - return NewNumericReply(channel.server, RPL_NOTOPIC, channel.name+" :No topic is set") + return NewNumericReply(channel.server, RPL_NOTOPIC, + "%s :No topic is set", channel.name) } func RplTopic(channel *Channel) Reply { return NewNumericReply(channel.server, RPL_TOPIC, - fmt.Sprintf("%s :%s", channel.name, channel.topic)) + "%s :%s", channel.name, channel.topic) } func RplInvitingMsg(channel *Channel, invitee *Client) Reply { return NewNumericReply(channel.server, RPL_INVITING, - fmt.Sprintf("%s %s", channel.name, invitee.Nick())) + "%s %s", channel.name, invitee.Nick()) } func RplNamReply(channel *Channel) Reply { // TODO multiple names and splitting based on message size return NewNumericReply(channel.server, RPL_NAMREPLY, - fmt.Sprintf("= %s :%s", channel.name, strings.Join(channel.Nicks(), " "))) + "= %s :%s", channel.name, strings.Join(channel.Nicks(), " ")) } func RplEndOfNames(source Identifier) Reply { - return NewNumericReply(source, RPL_ENDOFNAMES, ":End of NAMES list") + return NewNumericReply(source, RPL_ENDOFNAMES, + ":End of NAMES list") } func RplYoureOper(server *Server) Reply { - return NewNumericReply(server, RPL_YOUREOPER, ":You are now an IRC operator") + return NewNumericReply(server, RPL_YOUREOPER, + ":You are now an IRC operator") } // errors (also numeric) func ErrAlreadyRegistered(source Identifier) Reply { - return NewNumericReply(source, ERR_ALREADYREGISTRED, ":You may not reregister") + return NewNumericReply(source, ERR_ALREADYREGISTRED, + ":You may not reregister") } func ErrNickNameInUse(source Identifier, nick string) Reply { return NewNumericReply(source, ERR_NICKNAMEINUSE, - nick+" :Nickname is already in use") + "%s :Nickname is already in use", nick) } func ErrUnknownCommand(source Identifier, command string) Reply { return NewNumericReply(source, ERR_UNKNOWNCOMMAND, - command+" :Unknown command") + "%s :Unknown command", command) } func ErrUsersDontMatch(source Identifier) Reply { @@ -153,37 +160,37 @@ func ErrUsersDontMatch(source Identifier) Reply { func ErrNeedMoreParams(source Identifier, command string) Reply { return NewNumericReply(source, ERR_NEEDMOREPARAMS, - command+"%s :Not enough parameters") + "%s :Not enough parameters", command) } func ErrNoSuchChannel(source Identifier, channel string) Reply { return NewNumericReply(source, ERR_NOSUCHCHANNEL, - channel+" :No such channel") + "%s :No such channel", channel) } func ErrUserOnChannel(channel *Channel, member *Client) Reply { return NewNumericReply(channel.server, ERR_USERONCHANNEL, - fmt.Sprintf("%s %s :is already on channel", member.nick, channel.name)) + "%s %s :is already on channel", member.nick, channel.name) } func ErrNotOnChannel(channel *Channel) Reply { return NewNumericReply(channel.server, ERR_NOTONCHANNEL, - channel.name+" :You're not on that channel") + "%s :You're not on that channel", channel.name) } func ErrInviteOnlyChannel(channel *Channel) Reply { return NewNumericReply(channel.server, ERR_INVITEONLYCHAN, - channel.name+" :Cannot join channel (+i)") + "%s :Cannot join channel (+i)", channel.name) } func ErrBadChannelKey(channel *Channel) Reply { return NewNumericReply(channel.server, ERR_BADCHANNELKEY, - channel.name+" :Cannot join channel (+k)") + "%s :Cannot join channel (+k)", channel.name) } func ErrNoSuchNick(source Identifier, nick string) Reply { return NewNumericReply(source, ERR_NOSUCHNICK, - nick+" :No such nick/channel") + "%s :No such nick/channel", nick) } func ErrPasswdMismatch(server *Server) Reply { @@ -192,5 +199,13 @@ func ErrPasswdMismatch(server *Server) Reply { func ErrNoChanModes(channel *Channel) Reply { return NewNumericReply(channel.server, ERR_NOCHANMODES, - channel.name+" :Channel doesn't support modes") + "%s :Channel doesn't support modes", channel.name) +} + +func ErrNoPrivileges(server *Server) Reply { + return NewNumericReply(server, ERR_NOPRIVILEGES, ":Permission Denied") +} + +func ErrRestricted(server *Server) Reply { + return NewNumericReply(server, ERR_RESTRICTED, ":Your connection is restricted!") } diff --git a/src/irc/server.go b/src/irc/server.go index 40abc3ff..adb44a4b 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -1,39 +1,46 @@ package irc import ( + "code.google.com/p/go.crypto/bcrypt" "log" "net" "time" ) +type ClientNameMap map[string]*Client +type ChannelNameMap map[string]*Channel +type UserNameMap map[string]*User + type Server struct { hostname string ctime time.Time name string - recv chan<- Message - password string - nicks map[string]*Client - channels map[string]*Channel + commands chan<- Command + password []byte + users UserNameMap + channels ChannelNameMap } func NewServer(name string) *Server { - recv := make(chan Message) + commands := make(chan Command) server := &Server{ ctime: time.Now(), name: name, - recv: recv, - nicks: make(map[string]*Client), - channels: make(map[string]*Channel), + commands: commands, + users: make(UserNameMap), + channels: make(ChannelNameMap), } - go func() { - for m := range recv { - m.Client().atime = time.Now() - m.Handle(server, m.Client()) - } - }() + go server.receiveCommands(commands) return server } +func (server *Server) receiveCommands(commands <-chan Command) { + for command := range commands { + command.Client().atime = time.Now() + command.Handle(server) + } +} + func (s *Server) Listen(addr string) { listener, err := net.Listen("tcp", addr) if err != nil { @@ -50,7 +57,7 @@ func (s *Server) Listen(addr string) { continue } log.Print("Server.Listen: accepted ", conn.RemoteAddr()) - go NewClient(s, conn) + NewClient(s, conn) } } @@ -66,84 +73,220 @@ func (s *Server) GetOrMakeChannel(name string) *Channel { } // Send a message to clients of channels fromClient is a member. -func (s *Server) SendToInterestedClients(fromClient *Client, reply Reply) { - - clients := make(ClientSet) - clients[fromClient] = true - for channel := range fromClient.channels { - for client := range channel.members { - clients[client] = true +func (s *Server) InterestedUsers(fromUser *User) UserSet { + users := make(UserSet) + users[fromUser] = true + for channel := range fromUser.channels { + for user := range channel.members { + users[user] = true } } - for client := range clients { - client.send <- reply - } + return users } // server functionality -func (s *Server) ChangeNick(c *Client, newNick string) { - if s.nicks[newNick] != nil { - c.send <- ErrNickNameInUse(s, newNick) - return - } - s.SendToInterestedClients(c, RplNick(c, newNick)) - - if c.nick != "" { - delete(s.nicks, c.nick) - } - s.nicks[newNick] = c - c.nick = newNick - - s.tryRegister(c) -} - -func (s *Server) UserLogin(c *Client, user string, realName string) { - if c.username != "" { - c.send <- ErrAlreadyRegistered(s) - return - } - - c.username, c.realname = user, realName - s.tryRegister(c) -} - func (s *Server) tryRegister(c *Client) { - if !c.registered && c.HasNick() && c.HasUser() && (s.password == "" || c.serverPass) { + if !c.registered && c.HasNick() && c.HasUser() && (s.password == nil || c.serverPass) { c.registered = true - c.send <- RplWelcome(s, c) - c.send <- RplYourHost(s, c) - c.send <- RplCreated(s) - c.send <- RplMyInfo(s) + replies := []Reply{RplWelcome(s, c), RplYourHost(s, c), RplCreated(s), RplMyInfo(s)} + for _, reply := range replies { + c.replies <- reply + } } } -func (s *Server) Quit(c *Client, message string) { - for channel := range c.channels { - channel.Part(c, message) - } - delete(s.nicks, c.nick) - s.SendToInterestedClients(c, RplQuit(c, message)) - c.conn.Close() -} - func (s *Server) ChangeUserMode(c *Client, modes []string) { - for _, mode := range modes { - switch mode { - case "+w": - c.wallOps = true - case "-w": - c.wallOps = false - } - } - c.send <- RplUModeIs(s, c) + // Don't allow any mode changes. + c.replies <- RplUModeIs(s, c) } func (s *Server) Id() string { return s.hostname } +func (s *Server) PublicId() string { + return s.Id() +} + func (s *Server) DeleteChannel(channel *Channel) { delete(s.channels, channel.name) } + +// +// commands +// + +func (m *PingCommand) Handle(s *Server) { + m.Client().replies <- RplPong(s) +} + +func (m *PongCommand) Handle(s *Server) { + // no-op +} + +func (m *PassCommand) Handle(s *Server) { + err := bcrypt.CompareHashAndPassword(s.password, []byte(m.password)) + if err != nil { + m.Client().replies <- ErrPasswdMismatch(s) + return + } + + m.Client().serverPass = true + // no reply? +} + +func (m *NickCommand) Handle(s *Server) { + c := m.Client() + if c.user == nil { + c.replies <- RplNick(c, m.nickname) + c.nick = m.nickname + s.tryRegister(c) + return + } + + c.user.replies <- ErrNoPrivileges(s) +} + +func (m *UserCommand) Handle(s *Server) { + c := m.Client() + if c.username != "" { + c.replies <- ErrAlreadyRegistered(s) + return + } + + c.username, c.realname = m.user, m.realname + s.tryRegister(c) +} + +func (m *QuitCommand) Handle(s *Server) { + c := m.Client() + reply := RplQuit(c, m.message) + for user := range s.InterestedUsers(c.user) { + user.replies <- reply + } + c.conn.Close() + user := c.user + user.LogoutClient(c) + + if !user.HasClients() { + cmd := &PartChannelCommand{ + Command: m, + } + for channel := range c.user.channels { + channel.commands <- cmd + } + } +} + +func (m *JoinCommand) Handle(s *Server) { + c := m.Client() + if m.zero { + cmd := &PartChannelCommand{ + Command: m, + } + for channel := range c.user.channels { + channel.commands <- cmd + } + } else { + for i, name := range m.channels { + key := "" + if len(m.keys) > i { + key = m.keys[i] + } + + s.GetOrMakeChannel(name).commands <- &JoinChannelCommand{m, key} + } + } +} + +func (m *PartCommand) Handle(s *Server) { + user := m.Client().user + for _, chname := range m.channels { + channel := s.channels[chname] + + if channel == nil { + user.replies <- ErrNoSuchChannel(s, channel.name) + continue + } + + channel.commands <- &PartChannelCommand{m, m.message} + } +} + +func (m *TopicCommand) Handle(s *Server) { + user := m.Client().user + channel := s.channels[m.channel] + if channel == nil { + user.replies <- ErrNoSuchChannel(s, m.channel) + return + } + + if m.topic == "" { + channel.commands <- &GetTopicChannelCommand{m} + return + } + + channel.commands <- &SetTopicChannelCommand{m} +} + +func (m *PrivMsgCommand) Handle(s *Server) { + user := m.Client().user + + if m.TargetIsChannel() { + channel := s.channels[m.target] + if channel == nil { + user.replies <- ErrNoSuchNick(s, m.target) + return + } + + channel.commands <- &PrivMsgChannelCommand{m} + return + } + + target := s.users[m.target] + if target != nil { + target.replies <- ErrNoSuchNick(s, m.target) + return + } + + target.replies <- RplPrivMsg(user, target, m.message) +} + +func (m *LoginCommand) Handle(s *Server) { + client := m.Client() + if client.user != nil { + client.replies <- ErrAlreadyRegistered(s) + return + } + + user := s.users[m.nick] + if user == nil { + client.replies <- ErrNoSuchNick(s, m.nick) + return + } + + if !user.Login(client, m.nick, m.password) { + client.replies <- ErrRestricted(s) + return + } + + client.replies <- RplNick(client, m.nick) + // TODO join channels +} + +func (m *ReserveCommand) Handle(s *Server) { + client := m.Client() + if client.user != nil { + client.replies <- ErrAlreadyRegistered(s) + return + } + + if s.users[m.nick] != nil { + client.replies <- ErrNickNameInUse(s, m.nick) + return + } + + s.users[m.nick] = NewUser(m.nick, m.password, s) +} diff --git a/src/irc/user.go b/src/irc/user.go index a3e6ffb2..b29f0fbf 100644 --- a/src/irc/user.go +++ b/src/irc/user.go @@ -1,5 +1,100 @@ package irc +import ( + "code.google.com/p/go.crypto/bcrypt" + "fmt" +) + type User struct { - nick string + nick string + hash []byte + server *Server + replies chan<- Reply + commands <-chan Command + clients ClientSet + channels ChannelSet +} + +type UserSet map[*User]bool + +func (set UserSet) Add(user *User) { + set[user] = true +} + +func (set UserSet) Remove(user *User) { + delete(set, user) +} + +func NewUser(nick string, password string, server *Server) *User { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + panic("bcrypt failed; cannot generate password hash") + } + replies := make(chan Reply) + user := &User{ + nick: nick, + hash: hash, + server: server, + clients: make(ClientSet), + replies: replies, + } + go user.receiveReplies(replies) + return user +} + +// Distribute replies to clients. +func (user *User) receiveReplies(replies <-chan Reply) { + for reply := range replies { + for client := range user.clients { + client.replies <- reply + } + } +} + +// Identifier + +func (user *User) Id() string { + return fmt.Sprintf("%s!%s@%s", user.nick, user.nick, user.server.Id()) +} + +func (user *User) PublicId() string { + return user.Id() +} + +func (user *User) Nick() string { + return user.nick +} + +func (user *User) Login(c *Client, nick string, password string) bool { + if nick != c.nick { + return false + } + + if user.hash == nil { + return false + } + + err := bcrypt.CompareHashAndPassword(user.hash, []byte(password)) + if err != nil { + c.replies <- ErrNoPrivileges(user.server) + return false + } + + user.clients[c] = true + c.user = user + c.replies <- RplNick(c, user.nick) + // TODO join channels + return true +} + +func (user *User) LogoutClient(c *Client) bool { + if user.clients[c] { + delete(user.clients, c) + return true + } + return false +} + +func (user *User) HasClients() bool { + return len(user.clients) > 0 }