diff --git a/irc/client.go b/irc/client.go index d6074e63..c872f5f0 100644 --- a/irc/client.go +++ b/irc/client.go @@ -9,6 +9,8 @@ import ( "fmt" "net" "time" + + "github.com/DanielOaks/girc-go/ircmsg" ) const ( @@ -65,51 +67,44 @@ func NewClient(server *Server, conn net.Conn) *Client { func (client *Client) run() { var command Command var err error + var isExiting bool var line string + var msg ircmsg.IrcMessage // Set the hostname for this client. The client may later send a PROXY // command from stunnel that sets the hostname to something more accurate. client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr()) - for err == nil { - //TODO(dan): does this read sockets correctly and split lines properly? (think that ZNC bug that kept happening with mammon) - if line, err = client.socket.Read(); err != nil { - command = NewQuitCommand("connection closed") - - } else if command, err = ParseCommand(line); err != nil { - switch err { - case ErrParseCommand: - //TODO(dan): why is this a notice? there's a proper numeric for this I swear - client.Reply(RplNotice(client.server, client, - NewText("failed to parse command"))) - } - // so the read loop will continue - err = nil - continue - - } else if checkPass, ok := command.(checkPasswordCommand); ok { - checkPass.LoadPassword(client.server) - // Block the client thread while handling a potentially expensive - // password bcrypt operation. Since the server is single-threaded - // for commands, we don't want the server to perform the bcrypt, - // blocking anyone else from sending commands until it - // completes. This could be a form of DoS if handled naively. - checkPass.CheckPassword() + //TODO(dan): Make this a socketreactor from ircbnc + for { + line, err = client.socket.Read() + if err != nil { + client.Quit("connection closed") + break } - client.send(command) + msg, err = ParseLine(line) + if err != nil { + client.Quit("received malformed command") + break + } + + isExiting = Run(client.server, client, msg) + if isExiting { + break + } } + + // ensure client connection gets closed + client.Destroy() } -func (client *Client) send(command Command) { - command.SetClient(client) - client.server.commands <- command -} - +// // quit timer goroutine +// func (client *Client) connectionTimeout() { - client.send(NewQuitCommand("connection timeout")) + client.Quit("connection timeout") } // @@ -158,31 +153,6 @@ func (client *Client) Register() { client.Touch() } -func (client *Client) destroy() { - // clean up channels - - for channel := range client.channels { - channel.Quit(client) - } - - // clean up server - - client.server.clients.Remove(client) - - // clean up self - - if client.idleTimer != nil { - client.idleTimer.Stop() - } - if client.quitTimer != nil { - client.quitTimer.Stop() - } - - client.socket.Close() - - Log.debug.Printf("%s: destroyed", client) -} - func (client *Client) IdleTime() time.Duration { return time.Since(client.atime) } @@ -238,6 +208,7 @@ func (c *Client) String() string { return c.Id().String() } +// Friends refers to clients that share a channel with this client. func (client *Client) Friends() ClientSet { friends := make(ClientSet) friends.Add(client) @@ -276,16 +247,36 @@ func (client *Client) Reply(reply string) error { } func (client *Client) Quit(message Text) { - if client.hasQuit { + client.Send("QUIT", message) +} + +func (client *Client) destroy() { + if client.isDestroyed { return } - client.hasQuit = true - client.Reply(RplError("quit")) + client.isDestroyed = true client.server.whoWas.Append(client) friends := client.Friends() friends.Remove(client) - client.destroy() + + // clean up channels + for channel := range client.channels { + channel.Quit(client) + } + + // clean up server + client.server.clients.Remove(client) + + // clean up self + if client.idleTimer != nil { + client.idleTimer.Stop() + } + if client.quitTimer != nil { + client.quitTimer.Stop() + } + + client.socket.Close() if len(friends) > 0 { reply := RplQuit(client, message) diff --git a/irc/commandhandlers.go b/irc/commandhandlers.go new file mode 100644 index 00000000..948f482d --- /dev/null +++ b/irc/commandhandlers.go @@ -0,0 +1,550 @@ +// Copyright (c) 2012-2014 Jeremy Latt +// Copyright (c) 2014-2015 Edmund Huber +// Copyright (c) 2016- Daniel Oaks +// released under the MIT license + +package irc + +import ( + "fmt" + + "github.com/DanielOaks/girc-go/ircmsg" +) + +// NICK +func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + // check NICK validity + // send NICK change to primary server thread for processing + // |-> ensure no other client exists with that nickname + return true +} + +type ModeChange struct { + mode UserMode + op ModeOp +} + +func (change *ModeChange) String() string { + return fmt.Sprintf("%s%s", change.op, change.mode) +} + +type ModeChanges []*ModeChange + +func (changes ModeChanges) String() string { + if len(changes) == 0 { + return "" + } + + op := changes[0].op + str := changes[0].op.String() + for _, change := range changes { + if change.op != op { + op = change.op + str += change.op.String() + } + str += change.mode.String() + } + return str +} + +/* +type ModeCommand struct { + BaseCommand + nickname Name + changes ModeChanges +} + +// MODE ( "+" / "-" )? *( "+" / "-" / ) +func ParseUserModeCommand(nickname Name, args []string) (Command, error) { + cmd := &ModeCommand{ + nickname: nickname, + changes: make(ModeChanges, 0), + } + + // account for MODE command with no args to list things + if len(args) < 1 { + // don't do any further processing + return cmd, nil + } + + modeArg := args[0] + op := ModeOp(modeArg[0]) + if (op == Add) || (op == Remove) { + modeArg = modeArg[1:] + } else { + return nil, ErrParseCommand + } + + for _, mode := range modeArg { + if mode == '-' || mode == '+' { + op = ModeOp(mode) + continue + } + cmd.changes = append(cmd.changes, &ModeChange{ + mode: UserMode(mode), + op: op, + }) + } + + return cmd, nil +} +*/ + +type ChannelModeChange struct { + mode ChannelMode + op ModeOp + arg string +} + +func (change *ChannelModeChange) String() (str string) { + if (change.op == Add) || (change.op == Remove) { + str = change.op.String() + } + str += change.mode.String() + if change.arg != "" { + str += " " + change.arg + } + return +} + +type ChannelModeChanges []*ChannelModeChange + +func (changes ChannelModeChanges) String() string { + if len(changes) == 0 { + return "" + } + + op := changes[0].op + str := changes[0].op.String() + + for _, change := range changes { + if change.op != op { + op = change.op + str += change.op.String() + } + str += change.mode.String() + } + + for _, change := range changes { + if change.arg == "" { + continue + } + str += " " + change.arg + } + return str +} + +type ChannelModeCommand struct { + channel Name + changes ChannelModeChanges +} + +// MODE ( "+" / "-" )? *( "+" / "-" / ) * +func ParseChannelModeCommand(channel Name, args []string) (Command, error) { + cmd := &ChannelModeCommand{ + channel: channel, + changes: make(ChannelModeChanges, 0), + } + + // account for MODE command with no args to list things + if len(args) < 1 { + // don't do any further processing + return cmd, nil + } + + modeArg := args[0] + op := ModeOp(modeArg[0]) + if (op == Add) || (op == Remove) { + modeArg = modeArg[1:] + } else { + return nil, ErrParseCommand + } + + currentArgIndex := 1 + + for _, mode := range modeArg { + if mode == '-' || mode == '+' { + op = ModeOp(mode) + continue + } + change := &ChannelModeChange{ + mode: ChannelMode(mode), + op: op, + } + switch change.mode { + // TODO(dan): separate this into the type A/B/C/D args and use those lists here + case Key, BanMask, ExceptMask, InviteMask, UserLimit, + ChannelOperator, ChannelFounder, ChannelAdmin, Halfop, Voice: + if len(args) > currentArgIndex { + change.arg = args[currentArgIndex] + currentArgIndex++ + } else { + // silently skip this mode + continue + } + } + cmd.changes = append(cmd.changes, change) + } + + return cmd, nil +} + +/* + +func ParseModeCommand(args []string) (Command, error) { + name := NewName(args[0]) + if name.IsChannel() { + return ParseChannelModeCommand(name, args[1:]) + } else { + return ParseUserModeCommand(name, args[1:]) + } +} + +type WhoisCommand struct { + BaseCommand + target Name + masks []Name +} + +// WHOIS [ ] *( "," ) +func ParseWhoisCommand(args []string) (Command, error) { + var masks string + var target string + + if len(args) > 1 { + target = args[0] + masks = args[1] + } else { + masks = args[0] + } + + return &WhoisCommand{ + target: NewName(target), + masks: NewNames(strings.Split(masks, ",")), + }, nil +} + +type WhoCommand struct { + BaseCommand + mask Name + operatorOnly bool +} + +// WHO [ [ "o" ] ] +func ParseWhoCommand(args []string) (Command, error) { + cmd := &WhoCommand{} + + if len(args) > 0 { + cmd.mask = NewName(args[0]) + } + + if (len(args) > 1) && (args[1] == "o") { + cmd.operatorOnly = true + } + + return cmd, nil +} + +type OperCommand struct { + PassCommand + name Name +} + +func (msg *OperCommand) LoadPassword(server *Server) { + msg.hash = server.operators[msg.name] +} + +// OPER +func ParseOperCommand(args []string) (Command, error) { + cmd := &OperCommand{ + name: NewName(args[0]), + } + cmd.password = []byte(args[1]) + return cmd, nil +} + +type CapCommand struct { + BaseCommand + subCommand CapSubCommand + capabilities CapabilitySet +} + +func ParseCapCommand(args []string) (Command, error) { + cmd := &CapCommand{ + subCommand: CapSubCommand(strings.ToUpper(args[0])), + capabilities: make(CapabilitySet), + } + + if len(args) > 1 { + strs := spacesExpr.Split(args[1], -1) + for _, str := range strs { + cmd.capabilities[Capability(str)] = true + } + } + return cmd, nil +} + +// HAPROXY support +type ProxyCommand struct { + BaseCommand + net Name + sourceIP Name + destIP Name + sourcePort Name + destPort Name + hostname Name // looked up in socket thread +} + +func NewProxyCommand(hostname Name) *ProxyCommand { + cmd := &ProxyCommand{ + hostname: hostname, + } + cmd.code = PROXY + return cmd +} + +func ParseProxyCommand(args []string) (Command, error) { + return &ProxyCommand{ + net: NewName(args[0]), + sourceIP: NewName(args[1]), + destIP: NewName(args[2]), + sourcePort: NewName(args[3]), + destPort: NewName(args[4]), + hostname: LookupHostname(NewName(args[1])), + }, nil +} + +type AwayCommand struct { + BaseCommand + text Text +} + +func ParseAwayCommand(args []string) (Command, error) { + cmd := &AwayCommand{} + + if len(args) > 0 { + cmd.text = NewText(args[0]) + } + + return cmd, nil +} + +type IsOnCommand struct { + BaseCommand + nicks []Name +} + +func ParseIsOnCommand(args []string) (Command, error) { + return &IsOnCommand{ + nicks: NewNames(args), + }, nil +} + +type MOTDCommand struct { + BaseCommand + target Name +} + +func ParseMOTDCommand(args []string) (Command, error) { + cmd := &MOTDCommand{} + if len(args) > 0 { + cmd.target = NewName(args[0]) + } + return cmd, nil +} + +type NoticeCommand struct { + BaseCommand + target Name + message Text +} + +func ParseNoticeCommand(args []string) (Command, error) { + return &NoticeCommand{ + target: NewName(args[0]), + message: NewText(args[1]), + }, nil +} + +type KickCommand struct { + BaseCommand + kicks map[Name]Name + comment Text +} + +func (msg *KickCommand) Comment() Text { + if msg.comment == "" { + return msg.Client().Nick().Text() + } + return msg.comment +} + +func ParseKickCommand(args []string) (Command, error) { + channels := NewNames(strings.Split(args[0], ",")) + users := NewNames(strings.Split(args[1], ",")) + if (len(channels) != len(users)) && (len(users) != 1) { + return nil, NotEnoughArgsError + } + cmd := &KickCommand{ + kicks: make(map[Name]Name), + } + for index, channel := range channels { + if len(users) == 1 { + cmd.kicks[channel] = users[0] + } else { + cmd.kicks[channel] = users[index] + } + } + if len(args) > 2 { + cmd.comment = NewText(args[2]) + } + return cmd, nil +} + +type ListCommand struct { + BaseCommand + channels []Name + target Name +} + +func ParseListCommand(args []string) (Command, error) { + cmd := &ListCommand{} + if len(args) > 0 { + cmd.channels = NewNames(strings.Split(args[0], ",")) + } + if len(args) > 1 { + cmd.target = NewName(args[1]) + } + return cmd, nil +} + +type NamesCommand struct { + BaseCommand + channels []Name + target Name +} + +func ParseNamesCommand(args []string) (Command, error) { + cmd := &NamesCommand{} + if len(args) > 0 { + cmd.channels = NewNames(strings.Split(args[0], ",")) + } + if len(args) > 1 { + cmd.target = NewName(args[1]) + } + return cmd, nil +} + +type DebugCommand struct { + BaseCommand + subCommand Name +} + +func ParseDebugCommand(args []string) (Command, error) { + return &DebugCommand{ + subCommand: NewName(strings.ToUpper(args[0])), + }, nil +} + +type VersionCommand struct { + BaseCommand + target Name +} + +func ParseVersionCommand(args []string) (Command, error) { + cmd := &VersionCommand{} + if len(args) > 0 { + cmd.target = NewName(args[0]) + } + return cmd, nil +} + +type InviteCommand struct { + BaseCommand + nickname Name + channel Name +} + +func ParseInviteCommand(args []string) (Command, error) { + return &InviteCommand{ + nickname: NewName(args[0]), + channel: NewName(args[1]), + }, nil +} + +func ParseTheaterCommand(args []string) (Command, error) { + if upperSubCmd := strings.ToUpper(args[0]); upperSubCmd == "IDENTIFY" && len(args) == 3 { + return &TheaterIdentifyCommand{ + channel: NewName(args[1]), + PassCommand: PassCommand{password: []byte(args[2])}, + }, nil + } else if upperSubCmd == "PRIVMSG" && len(args) == 4 { + return &TheaterPrivMsgCommand{ + channel: NewName(args[1]), + asNick: NewName(args[2]), + message: NewText(args[3]), + }, nil + } else if upperSubCmd == "ACTION" && len(args) == 4 { + return &TheaterActionCommand{ + channel: NewName(args[1]), + asNick: NewName(args[2]), + action: NewCTCPText(args[3]), + }, nil + } else { + return nil, ErrParseCommand + } +} + +type TimeCommand struct { + BaseCommand + target Name +} + +func ParseTimeCommand(args []string) (Command, error) { + cmd := &TimeCommand{} + if len(args) > 0 { + cmd.target = NewName(args[0]) + } + return cmd, nil +} + +type KillCommand struct { + BaseCommand + nickname Name + comment Text +} + +func ParseKillCommand(args []string) (Command, error) { + return &KillCommand{ + nickname: NewName(args[0]), + comment: NewText(args[1]), + }, nil +} + +type WhoWasCommand struct { + BaseCommand + nicknames []Name + count int64 + target Name +} + +func ParseWhoWasCommand(args []string) (Command, error) { + cmd := &WhoWasCommand{ + nicknames: NewNames(strings.Split(args[0], ",")), + } + if len(args) > 1 { + cmd.count, _ = strconv.ParseInt(args[1], 10, 64) + } + if len(args) > 2 { + cmd.target = NewName(args[2]) + } + return cmd, nil +} + +func ParseOperNickCommand(args []string) (Command, error) { + return &OperNickCommand{ + target: NewName(args[0]), + nick: NewName(args[1]), + }, nil +} +*/ diff --git a/irc/commands.go b/irc/commands.go index b1debd21..a7cec543 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -5,923 +5,176 @@ package irc -import ( - "errors" - "fmt" - "regexp" - "strconv" - "strings" -) +import "github.com/DanielOaks/girc-go/ircmsg" -type Command interface { - Client() *Client - Code() StringCode - SetClient(*Client) - SetCode(StringCode) +// Command represents a command accepted on a listener. +type Command struct { + handler func(client *Client, msg ircmsg.IrcMessage) bool + usablePreReg bool + minParams int } -type checkPasswordCommand interface { - LoadPassword(*Server) - CheckPassword() -} - -type parseCommandFunc func([]string) (Command, error) - -var ( - NotEnoughArgsError = errors.New("not enough arguments") - ErrParseCommand = errors.New("failed to parse message") - parseCommandFuncs = map[StringCode]parseCommandFunc{ - AWAY: ParseAwayCommand, - CAP: ParseCapCommand, - DEBUG: ParseDebugCommand, - INVITE: ParseInviteCommand, - ISON: ParseIsOnCommand, - JOIN: ParseJoinCommand, - KICK: ParseKickCommand, - KILL: ParseKillCommand, - LIST: ParseListCommand, - MODE: ParseModeCommand, - MOTD: ParseMOTDCommand, - NAMES: ParseNamesCommand, - NICK: ParseNickCommand, - NOTICE: ParseNoticeCommand, - ONICK: ParseOperNickCommand, - OPER: ParseOperCommand, - PART: ParsePartCommand, - PASS: ParsePassCommand, - PING: ParsePingCommand, - PONG: ParsePongCommand, - PRIVMSG: ParsePrivMsgCommand, - PROXY: ParseProxyCommand, - QUIT: ParseQuitCommand, - THEATER: ParseTheaterCommand, // nonstandard - TIME: ParseTimeCommand, - TOPIC: ParseTopicCommand, - USER: ParseUserCommand, - VERSION: ParseVersionCommand, - WHO: ParseWhoCommand, - WHOIS: ParseWhoisCommand, - WHOWAS: ParseWhoWasCommand, +// Run runs this command with the given listener/message. +func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + if !client.Registered && !cmd.usablePreReg { + // command silently ignored + return false } - commandMinimumArgs = map[StringCode]int{ - AWAY: 0, - CAP: 1, - DEBUG: 1, - INVITE: 2, - ISON: 1, - JOIN: 1, - KICK: 2, - KILL: 2, - LIST: 0, - MODE: 1, - MOTD: 0, - NAMES: 0, - NICK: 1, - NOTICE: 2, - ONICK: 2, - OPER: 2, - PART: 1, - PASS: 1, - PING: 1, - PONG: 1, - PRIVMSG: 2, - PROXY: 5, - QUIT: 0, - THEATER: 1, - TIME: 0, - TOPIC: 1, - USER: 4, - VERSION: 0, - WHO: 0, - WHOIS: 1, - WHOWAS: 1, + if len(msg.Params) < cmd.minParams { + listener.Send(nil, "", "461", client.Nick, msg.Command, "Not enough parameters") + return false } -) + exiting := cmd.handler(server, client, msg) -type BaseCommand struct { - client *Client - code StringCode -} - -func (command *BaseCommand) Client() *Client { - return command.client -} - -func (command *BaseCommand) SetClient(client *Client) { - command.client = client -} - -func (command *BaseCommand) Code() StringCode { - return command.code -} - -func (command *BaseCommand) SetCode(code StringCode) { - command.code = code -} - -type NeedMoreParamsCommand struct { - BaseCommand - code StringCode -} - -func ParseNeedMoreParams(code StringCode) *NeedMoreParamsCommand { - return &NeedMoreParamsCommand{ - code: code, - } -} - -func ParseCommand(line string) (cmd Command, err error) { - code, args := ParseLine(line) - constructor := parseCommandFuncs[code] - minArgs := commandMinimumArgs[code] - if constructor == nil { - cmd = ParseUnknownCommand(args) - } else if len(args) < minArgs { - cmd = ParseNeedMoreParams(code) - } else { - cmd, err = constructor(args) - - // if NotEnoughArgsError was returned in the command handler itself - if err == NotEnoughArgsError { - cmd = ParseNeedMoreParams(code) - err = nil - } - } - if cmd != nil { - cmd.SetCode(code) - } - return -} - -var ( - spacesExpr = regexp.MustCompile(` +`) -) - -func splitArg(line string) (arg string, rest string) { - parts := spacesExpr.Split(line, 2) - if len(parts) > 0 { - arg = parts[0] - } - if len(parts) > 1 { - rest = parts[1] - } - return -} - -func ParseLine(line string) (command StringCode, args []string) { - args = make([]string, 0) - if strings.HasPrefix(line, ":") { - _, line = splitArg(line) - } - arg, line := splitArg(line) - command = StringCode(NewName(strings.ToUpper(arg))) - for len(line) > 0 { - if strings.HasPrefix(line, ":") { - args = append(args, line[len(":"):]) - break - } - arg, line = splitArg(line) - args = append(args, arg) - } - return -} - -// [args...] - -type UnknownCommand struct { - BaseCommand - args []string -} - -func ParseUnknownCommand(args []string) *UnknownCommand { - return &UnknownCommand{ - args: args, - } -} - -// PING [ ] - -type PingCommand struct { - BaseCommand - server Name - server2 Name -} - -func ParsePingCommand(args []string) (Command, error) { - msg := &PingCommand{ - server: NewName(args[0]), - } - if len(args) > 1 { - msg.server2 = NewName(args[1]) - } - return msg, nil -} - -// PONG [ ] - -type PongCommand struct { - BaseCommand - server1 Name - server2 Name -} - -func ParsePongCommand(args []string) (Command, error) { - message := &PongCommand{ - server1: NewName(args[0]), - } - if len(args) > 1 { - message.server2 = NewName(args[1]) - } - return message, nil -} - -// PASS - -type PassCommand struct { - BaseCommand - hash []byte - password []byte - err error -} - -func (cmd *PassCommand) LoadPassword(server *Server) { - cmd.hash = server.password -} - -func (cmd *PassCommand) CheckPassword() { - if cmd.hash == nil { - return - } - cmd.err = ComparePassword(cmd.hash, cmd.password) -} - -func ParsePassCommand(args []string) (Command, error) { - return &PassCommand{ - password: []byte(args[0]), - }, nil -} - -// NICK - -func ParseNickCommand(args []string) (Command, error) { - return &NickCommand{ - nickname: NewName(args[0]), - }, nil -} - -type UserCommand struct { - BaseCommand - username Name - realname Text -} - -func ParseUserCommand(args []string) (Command, error) { - return &UserCommand{ - username: NewName(args[0]), - realname: NewText(args[3]), - }, nil -} - -// QUIT [ ] - -type QuitCommand struct { - BaseCommand - message Text -} - -func NewQuitCommand(message Text) *QuitCommand { - cmd := &QuitCommand{ - message: message, - } - cmd.code = QUIT - return cmd -} - -func ParseQuitCommand(args []string) (Command, error) { - msg := &QuitCommand{} - if len(args) > 0 { - msg.message = NewText(args[0]) - } - return msg, nil -} - -// JOIN ( *( "," ) [ *( "," ) ] ) / "0" - -type JoinCommand struct { - BaseCommand - channels map[Name]Text - zero bool -} - -func ParseJoinCommand(args []string) (Command, error) { - msg := &JoinCommand{ - channels: make(map[Name]Text), - } - - if args[0] == "0" { - msg.zero = true - return msg, nil - } - - channels := strings.Split(args[0], ",") - keys := make([]string, len(channels)) - if len(args) > 1 { - for i, key := range strings.Split(args[1], ",") { - if i >= len(channels) { + // after each command, see if we can send registration to the client + if !client.Registered { + isRegistered := true + for _, fulfilled := range client.regLocks { + if !fulfilled { + isRegistered = false break } - keys[i] = key + } + if isRegistered { + client.DumpRegistration() } } - for i, channel := range channels { - msg.channels[NewName(channel)] = NewText(keys[i]) - } - return msg, nil + return exiting } -// PART *( "," ) [ ] - -type PartCommand struct { - BaseCommand - channels []Name - message Text -} - -func (cmd *PartCommand) Message() Text { - if cmd.message == "" { - return cmd.Client().Nick().Text() - } - return cmd.message -} - -func ParsePartCommand(args []string) (Command, error) { - msg := &PartCommand{ - channels: NewNames(strings.Split(args[0], ",")), - } - if len(args) > 1 { - msg.message = NewText(args[1]) - } - return msg, nil -} - -// PRIVMSG - -type PrivMsgCommand struct { - BaseCommand - target Name - message Text -} - -func ParsePrivMsgCommand(args []string) (Command, error) { - return &PrivMsgCommand{ - target: NewName(args[0]), - message: NewText(args[1]), - }, nil -} - -// TOPIC [newtopic] - -type TopicCommand struct { - BaseCommand - channel Name - setTopic bool - topic Text -} - -func ParseTopicCommand(args []string) (Command, error) { - msg := &TopicCommand{ - channel: NewName(args[0]), - } - if len(args) > 1 { - msg.setTopic = true - msg.topic = NewText(args[1]) - } - return msg, nil -} - -type ModeChange struct { - mode UserMode - op ModeOp -} - -func (change *ModeChange) String() string { - return fmt.Sprintf("%s%s", change.op, change.mode) -} - -type ModeChanges []*ModeChange - -func (changes ModeChanges) String() string { - if len(changes) == 0 { - return "" - } - - op := changes[0].op - str := changes[0].op.String() - for _, change := range changes { - if change.op != op { - op = change.op - str += change.op.String() - } - str += change.mode.String() - } - return str -} - -type ModeCommand struct { - BaseCommand - nickname Name - changes ModeChanges -} - -// MODE ( "+" / "-" )? *( "+" / "-" / ) -func ParseUserModeCommand(nickname Name, args []string) (Command, error) { - cmd := &ModeCommand{ - nickname: nickname, - changes: make(ModeChanges, 0), - } - - // account for MODE command with no args to list things - if len(args) < 1 { - // don't do any further processing - return cmd, nil - } - - modeArg := args[0] - op := ModeOp(modeArg[0]) - if (op == Add) || (op == Remove) { - modeArg = modeArg[1:] - } else { - return nil, ErrParseCommand - } - - for _, mode := range modeArg { - if mode == '-' || mode == '+' { - op = ModeOp(mode) - continue - } - cmd.changes = append(cmd.changes, &ModeChange{ - mode: UserMode(mode), - op: op, - }) - } - - return cmd, nil -} - -type ChannelModeChange struct { - mode ChannelMode - op ModeOp - arg string -} - -func (change *ChannelModeChange) String() (str string) { - if (change.op == Add) || (change.op == Remove) { - str = change.op.String() - } - str += change.mode.String() - if change.arg != "" { - str += " " + change.arg - } - return -} - -type ChannelModeChanges []*ChannelModeChange - -func (changes ChannelModeChanges) String() string { - if len(changes) == 0 { - return "" - } - - op := changes[0].op - str := changes[0].op.String() - - for _, change := range changes { - if change.op != op { - op = change.op - str += change.op.String() - } - str += change.mode.String() - } - - for _, change := range changes { - if change.arg == "" { - continue - } - str += " " + change.arg - } - return str -} - -type ChannelModeCommand struct { - BaseCommand - channel Name - changes ChannelModeChanges -} - -// MODE ( "+" / "-" )? *( "+" / "-" / ) * -func ParseChannelModeCommand(channel Name, args []string) (Command, error) { - cmd := &ChannelModeCommand{ - channel: channel, - changes: make(ChannelModeChanges, 0), - } - - // account for MODE command with no args to list things - if len(args) < 1 { - // don't do any further processing - return cmd, nil - } - - modeArg := args[0] - op := ModeOp(modeArg[0]) - if (op == Add) || (op == Remove) { - modeArg = modeArg[1:] - } else { - return nil, ErrParseCommand - } - - currentArgIndex := 1 - - for _, mode := range modeArg { - if mode == '-' || mode == '+' { - op = ModeOp(mode) - continue - } - change := &ChannelModeChange{ - mode: ChannelMode(mode), - op: op, - } - switch change.mode { - // TODO(dan): separate this into the type A/B/C/D args and use those lists here - case Key, BanMask, ExceptMask, InviteMask, UserLimit, - ChannelOperator, ChannelFounder, ChannelAdmin, Halfop, Voice: - if len(args) > currentArgIndex { - change.arg = args[currentArgIndex] - currentArgIndex++ - } else { - // silently skip this mode - continue - } - } - cmd.changes = append(cmd.changes, change) - } - - return cmd, nil -} - -func ParseModeCommand(args []string) (Command, error) { - name := NewName(args[0]) - if name.IsChannel() { - return ParseChannelModeCommand(name, args[1:]) - } else { - return ParseUserModeCommand(name, args[1:]) - } -} - -type WhoisCommand struct { - BaseCommand - target Name - masks []Name -} - -// WHOIS [ ] *( "," ) -func ParseWhoisCommand(args []string) (Command, error) { - var masks string - var target string - - if len(args) > 1 { - target = args[0] - masks = args[1] - } else { - masks = args[0] - } - - return &WhoisCommand{ - target: NewName(target), - masks: NewNames(strings.Split(masks, ",")), - }, nil -} - -type WhoCommand struct { - BaseCommand - mask Name - operatorOnly bool -} - -// WHO [ [ "o" ] ] -func ParseWhoCommand(args []string) (Command, error) { - cmd := &WhoCommand{} - - if len(args) > 0 { - cmd.mask = NewName(args[0]) - } - - if (len(args) > 1) && (args[1] == "o") { - cmd.operatorOnly = true - } - - return cmd, nil -} - -type OperCommand struct { - PassCommand - name Name -} - -func (msg *OperCommand) LoadPassword(server *Server) { - msg.hash = server.operators[msg.name] -} - -// OPER -func ParseOperCommand(args []string) (Command, error) { - cmd := &OperCommand{ - name: NewName(args[0]), - } - cmd.password = []byte(args[1]) - return cmd, nil -} - -type CapCommand struct { - BaseCommand - subCommand CapSubCommand - capabilities CapabilitySet -} - -func ParseCapCommand(args []string) (Command, error) { - cmd := &CapCommand{ - subCommand: CapSubCommand(strings.ToUpper(args[0])), - capabilities: make(CapabilitySet), - } - - if len(args) > 1 { - strs := spacesExpr.Split(args[1], -1) - for _, str := range strs { - cmd.capabilities[Capability(str)] = true - } - } - return cmd, nil -} - -// HAPROXY support -type ProxyCommand struct { - BaseCommand - net Name - sourceIP Name - destIP Name - sourcePort Name - destPort Name - hostname Name // looked up in socket thread -} - -func NewProxyCommand(hostname Name) *ProxyCommand { - cmd := &ProxyCommand{ - hostname: hostname, - } - cmd.code = PROXY - return cmd -} - -func ParseProxyCommand(args []string) (Command, error) { - return &ProxyCommand{ - net: NewName(args[0]), - sourceIP: NewName(args[1]), - destIP: NewName(args[2]), - sourcePort: NewName(args[3]), - destPort: NewName(args[4]), - hostname: LookupHostname(NewName(args[1])), - }, nil -} - -type AwayCommand struct { - BaseCommand - text Text -} - -func ParseAwayCommand(args []string) (Command, error) { - cmd := &AwayCommand{} - - if len(args) > 0 { - cmd.text = NewText(args[0]) - } - - return cmd, nil -} - -type IsOnCommand struct { - BaseCommand - nicks []Name -} - -func ParseIsOnCommand(args []string) (Command, error) { - return &IsOnCommand{ - nicks: NewNames(args), - }, nil -} - -type MOTDCommand struct { - BaseCommand - target Name -} - -func ParseMOTDCommand(args []string) (Command, error) { - cmd := &MOTDCommand{} - if len(args) > 0 { - cmd.target = NewName(args[0]) - } - return cmd, nil -} - -type NoticeCommand struct { - BaseCommand - target Name - message Text -} - -func ParseNoticeCommand(args []string) (Command, error) { - return &NoticeCommand{ - target: NewName(args[0]), - message: NewText(args[1]), - }, nil -} - -type KickCommand struct { - BaseCommand - kicks map[Name]Name - comment Text -} - -func (msg *KickCommand) Comment() Text { - if msg.comment == "" { - return msg.Client().Nick().Text() - } - return msg.comment -} - -func ParseKickCommand(args []string) (Command, error) { - channels := NewNames(strings.Split(args[0], ",")) - users := NewNames(strings.Split(args[1], ",")) - if (len(channels) != len(users)) && (len(users) != 1) { - return nil, NotEnoughArgsError - } - cmd := &KickCommand{ - kicks: make(map[Name]Name), - } - for index, channel := range channels { - if len(users) == 1 { - cmd.kicks[channel] = users[0] - } else { - cmd.kicks[channel] = users[index] - } - } - if len(args) > 2 { - cmd.comment = NewText(args[2]) - } - return cmd, nil -} - -type ListCommand struct { - BaseCommand - channels []Name - target Name -} - -func ParseListCommand(args []string) (Command, error) { - cmd := &ListCommand{} - if len(args) > 0 { - cmd.channels = NewNames(strings.Split(args[0], ",")) - } - if len(args) > 1 { - cmd.target = NewName(args[1]) - } - return cmd, nil -} - -type NamesCommand struct { - BaseCommand - channels []Name - target Name -} - -func ParseNamesCommand(args []string) (Command, error) { - cmd := &NamesCommand{} - if len(args) > 0 { - cmd.channels = NewNames(strings.Split(args[0], ",")) - } - if len(args) > 1 { - cmd.target = NewName(args[1]) - } - return cmd, nil -} - -type DebugCommand struct { - BaseCommand - subCommand Name -} - -func ParseDebugCommand(args []string) (Command, error) { - return &DebugCommand{ - subCommand: NewName(strings.ToUpper(args[0])), - }, nil -} - -type VersionCommand struct { - BaseCommand - target Name -} - -func ParseVersionCommand(args []string) (Command, error) { - cmd := &VersionCommand{} - if len(args) > 0 { - cmd.target = NewName(args[0]) - } - return cmd, nil -} - -type InviteCommand struct { - BaseCommand - nickname Name - channel Name -} - -func ParseInviteCommand(args []string) (Command, error) { - return &InviteCommand{ - nickname: NewName(args[0]), - channel: NewName(args[1]), - }, nil -} - -func ParseTheaterCommand(args []string) (Command, error) { - if upperSubCmd := strings.ToUpper(args[0]); upperSubCmd == "IDENTIFY" && len(args) == 3 { - return &TheaterIdentifyCommand{ - channel: NewName(args[1]), - PassCommand: PassCommand{password: []byte(args[2])}, - }, nil - } else if upperSubCmd == "PRIVMSG" && len(args) == 4 { - return &TheaterPrivMsgCommand{ - channel: NewName(args[1]), - asNick: NewName(args[2]), - message: NewText(args[3]), - }, nil - } else if upperSubCmd == "ACTION" && len(args) == 4 { - return &TheaterActionCommand{ - channel: NewName(args[1]), - asNick: NewName(args[2]), - action: NewCTCPText(args[3]), - }, nil - } else { - return nil, ErrParseCommand - } -} - -type TimeCommand struct { - BaseCommand - target Name -} - -func ParseTimeCommand(args []string) (Command, error) { - cmd := &TimeCommand{} - if len(args) > 0 { - cmd.target = NewName(args[0]) - } - return cmd, nil -} - -type KillCommand struct { - BaseCommand - nickname Name - comment Text -} - -func ParseKillCommand(args []string) (Command, error) { - return &KillCommand{ - nickname: NewName(args[0]), - comment: NewText(args[1]), - }, nil -} - -type WhoWasCommand struct { - BaseCommand - nicknames []Name - count int64 - target Name -} - -func ParseWhoWasCommand(args []string) (Command, error) { - cmd := &WhoWasCommand{ - nicknames: NewNames(strings.Split(args[0], ",")), - } - if len(args) > 1 { - cmd.count, _ = strconv.ParseInt(args[1], 10, 64) - } - if len(args) > 2 { - cmd.target = NewName(args[2]) - } - return cmd, nil -} - -func ParseOperNickCommand(args []string) (Command, error) { - return &OperNickCommand{ - target: NewName(args[0]), - nick: NewName(args[1]), - }, nil +// Commands holds all commands executable by a client connected to us. +var Commands = map[string]Command{ + "AWAY": Command{ + handler: awayHandler, + minParams: 0, + }, + "CAP": Command{ + handler: capHandler, + usablePreReg: true, + minParams: 1, + }, + "DEBUG": Command{ + handler: debugHandler, + minParams: 1, + }, + "INVITE": Command{ + handler: inviteHandler, + minParams: 2, + }, + "ISON": Command{ + handler: isonHandler, + minParams: 1, + }, + "JOIN": Command{ + handler: joinHandler, + minParams: 1, + }, + "KICK": Command{ + handler: kickHandler, + minParams: 2, + }, + "KILL": Command{ + handler: killHandler, + minParams: 2, + }, + "LIST": Command{ + handler: listHandler, + minParams: 0, + }, + "MODE": Command{ + handler: modeHandler, + minParams: 1, + }, + "MOTD": Command{ + handler: motdHandler, + minParams: 0, + }, + "NAMES": Command{ + handler: namesHandler, + minParams: 0, + }, + "NICK": Command{ + handler: nickHandler, + usablePreReg: true, + minParams: 1, + }, + "NOTICE": Command{ + handler: noticeHandler, + minParams: 2, + }, + "ONICK": Command{ + handler: onickHandler, + minParams: 2, + }, + "OPER": Command{ + handler: operHandler, + minParams: 2, + }, + "PART": Command{ + handler: partHandler, + minParams: 1, + }, + "PASS": Command{ + handler: passHandler, + usablePreReg: true, + minParams: 1, + }, + "PING": Command{ + handler: pingHandler, + usablePreReg: true, + minParams: 1, + }, + "PONG": Command{ + handler: pongHandler, + usablePreReg: true, + minParams: 1, + }, + "PRIVMSG": Command{ + handler: privmsgHandler, + minParams: 2, + }, + "PROXY": Command{ + handler: proxyHandler, + usablePreReg: true, + minParams: 5, + }, + "QUIT": Command{ + handler: quitHandler, + usablePreReg: true, + minParams: 0, + }, + "THEATRE": Command{ + handler: theatreHandler, + minParams: 1, + }, + "TIME": Command{ + handler: timeHandler, + minParams: 0, + }, + "TOPIC": Command{ + handler: topicHandler, + minParams: 1, + }, + "USER": Command{ + handler: userHandler, + usablePreReg: true, + minParams: 4, + }, + "VERSION": Command{ + handler: versionHandler, + minParams: 0, + }, + "WHO": Command{ + handler: whoHandler, + minParams: 0, + }, + "WHOIS": Command{ + handler: whoisHandler, + minParams: 1, + }, + "WHOWAS": Command{ + handler: whowasHandler, + minParams: 1, + }, } diff --git a/irc/debug.go b/irc/debug.go index 3b5ed906..7903ef8a 100644 --- a/irc/debug.go +++ b/irc/debug.go @@ -9,15 +9,16 @@ import ( "runtime/debug" "runtime/pprof" "time" + + "github.com/DanielOaks/girc-go/ircmsg" ) -func (msg *DebugCommand) HandleServer(server *Server) { - client := msg.Client() +func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if !client.flags[Operator] { return } - switch msg.subCommand { + switch msg.Params[0] { case "GCSTATS": stats := debug.GCStats{ Pause: make([]time.Duration, 10), diff --git a/irc/modes.go b/irc/modes.go index 8ac084ba..b04b7c65 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -122,6 +122,7 @@ var ( // commands // +/* func (m *ModeCommand) HandleServer(s *Server) { client := m.Client() target := s.clients.Get(m.nickname) @@ -185,3 +186,4 @@ func (msg *ChannelModeCommand) HandleServer(server *Server) { channel.Mode(client, msg.changes) } +*/ diff --git a/irc/nickname.go b/irc/nickname.go index bf3d5bf4..85f43eaf 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -4,6 +4,7 @@ package irc +/* type NickCommand struct { BaseCommand nickname Name @@ -98,3 +99,4 @@ func (msg *OperNickCommand) HandleServer(server *Server) { target.ChangeNickname(msg.nick) } +*/ diff --git a/irc/server.go b/irc/server.go index 1646d747..bf0cf100 100644 --- a/irc/server.go +++ b/irc/server.go @@ -15,21 +15,14 @@ import ( "net/http" "os" "os/signal" + "strconv" "strings" "syscall" "time" + + "github.com/DanielOaks/girc-go/ircmsg" ) -type ServerCommand interface { - Command - HandleServer(*Server) -} - -type RegServerCommand interface { - Command - HandleRegServer(*Server) -} - type Server struct { channels ChannelNameMap clients *ClientLookupSet @@ -217,6 +210,7 @@ func (server *Server) processCommand(cmd Command) { func (server *Server) Shutdown() { server.db.Close() for _, client := range server.clients.byNick { + client.Send("notice") client.Reply(RplNotice(server, client, "shutting down")) } } @@ -331,6 +325,7 @@ func (s *Server) tryRegister(c *Client) { return } + c.Send("Intro to the network") c.Register() c.RplWelcome() c.RplYourHost() @@ -342,14 +337,17 @@ func (s *Server) tryRegister(c *Client) { func (server *Server) MOTD(client *Client) { if len(server.motdLines) < 1 { + c.Send("send") client.ErrNoMOTD() return } client.RplMOTDStart() for _, line := range server.motdLines { + c.Send("send") client.RplMOTD(line) } + c.Send("send") client.RplMOTDEnd() } @@ -365,103 +363,132 @@ func (s *Server) Nick() Name { return s.Id() } -func (server *Server) Reply(target *Client, message string) { - target.Reply(RplPrivMsg(server, target, NewText(message))) -} - -func (server *Server) Replyf(target *Client, format string, args ...interface{}) { - server.Reply(target, fmt.Sprintf(format, args...)) -} - // // registration commands // -func (msg *PassCommand) HandleRegServer(server *Server) { - client := msg.Client() - if msg.err != nil { - client.ErrPasswdMismatch() +// PASS +func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + if client.Registered { + client.Send("send") + client.ErrAlreadyRegistered() + return false + } + + // check the provided password + logger.Fatal("Implement PASS command") + password := []byte(args[0]) + if ComparePassword(server.password, password) != nil { + logger.Fatal("SEND BACK REJECTION") client.Quit("bad password") - return + return true } client.authorized = true + return false } -func (msg *ProxyCommand) HandleRegServer(server *Server) { - client := msg.Client() +// PROXY TCP4/6 SOURCEIP DESTIP SOURCEPORT DESTPORT +// http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt +func proxyHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { clientAddress := IPString(client.socket.conn.RemoteAddr()).String() clientHostname := client.hostname.String() for _, address := range server.proxyAllowedFrom { if clientHostname == address || clientAddress == address { - client.hostname = msg.hostname - return + client.hostname = LookupHostname(NewName(msg.Params[1])) + return false } } client.Quit("PROXY command is not usable from your address") + return true } -func (msg *UserCommand) HandleRegServer(server *Server) { - client := msg.Client() +// USER * 0 +func userHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + if client.Registered { + client.Send("send") + client.ErrAlreadyRegistered() + return false + } + if !client.authorized { - client.ErrPasswdMismatch() client.Quit("bad password") - return + return true + } + + if client.username != "" && client.realname != "" { + return false } // set user info and log client in - server.clients.Remove(client) //TODO(dan): Could there be a race condition here with adding/removing the client? - client.username, client.realname = msg.username, msg.realname + //TODO(dan): we should do something like server.clients.Replace(client) instead + + // we do it this way to ONLY replace what hasn't already been set + server.clients.Remove(client) + + if client.username != "" { + client.username = msg.username + } + if client.realname != "" { + client.realname = msg.realname + } + server.clients.Add(client) server.tryRegister(client) } -func (msg *QuitCommand) HandleRegServer(server *Server) { - msg.Client().Quit(msg.message) +// QUIT [] +func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + reason := "Quit" + if len(msg.Params) > 0 { + reason += ": " + msg.Params[0] + } + client.Quit(msg.message) + return true } // // normal commands // -func (m *PassCommand) HandleServer(s *Server) { - m.Client().ErrAlreadyRegistered() +// PING [] +func pingHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + // client.Socket.Send(response here) + return true } -func (m *PingCommand) HandleServer(s *Server) { - client := m.Client() - client.Reply(RplPong(client, m.server.Text())) +// PONG [ ] +func pongHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + //TODO(dan): update client idle timer from this + //TODO(dan): use this to affect how often we send pings + return true } -func (m *PongCommand) HandleServer(s *Server) { - // no-op -} - -func (m *UserCommand) HandleServer(s *Server) { - m.Client().ErrAlreadyRegistered() -} - -func (msg *QuitCommand) HandleServer(server *Server) { - msg.Client().Quit(msg.message) -} - -func (m *JoinCommand) HandleServer(s *Server) { - client := m.Client() - - if m.zero { +// JOIN {,} [{,}] +// JOIN 0 +func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + // handle JOIN 0 + if msg.Params[0] == "0" { for channel := range client.channels { channel.Part(client, client.Nick().Text()) } - return + return false } - for name, key := range m.channels { + // handle regular JOINs + channels := strings.Split(msg.Params[0], ",") + var keys []string + if len(msg.Params) > 1 { + keys = strings.Split(msg.Params[1], ",") + } + + for i, name := range channels { if !name.IsChannel() { - client.ErrNoSuchChannel(name) + log.Fatal("Implement ErrNoSuchChannel") continue } @@ -469,17 +496,29 @@ func (m *JoinCommand) HandleServer(s *Server) { if channel == nil { channel = NewChannel(s, name, true) } + + var key string + if len(keys) > i { + key = keys[i] + } + channel.Join(client, key) } } -func (m *PartCommand) HandleServer(server *Server) { - client := m.Client() - for _, chname := range m.channels { +// PART {,} [] +func partHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + channels := strings.Split(msg.Params[0], ",") + var reason string //TODO(dan): should this be the user's nickname instead of empty? + if len(msg.Params) > 1 { + reason = msg.Params[1] + } + + for _, chname := range channels { channel := server.channels.Get(chname) if channel == nil { - m.Client().ErrNoSuchChannel(chname) + log.Fatal("Implement ErrNoSuchChannel") continue } @@ -487,42 +526,47 @@ func (m *PartCommand) HandleServer(server *Server) { } } -func (msg *TopicCommand) HandleServer(server *Server) { - client := msg.Client() - channel := server.channels.Get(msg.channel) +// TOPIC [] +func topicHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + channel := server.channels.Get(msg.Params[0]) if channel == nil { - client.ErrNoSuchChannel(msg.channel) + log.Fatal("Implement ErrNoSuchChannel") return } - if msg.setTopic { - channel.SetTopic(client, msg.topic) + if len(msg.Params) > 1 { + channel.SetTopic(client, msg.Params[1]) } else { channel.GetTopic(client) } } -func (msg *PrivMsgCommand) HandleServer(server *Server) { - client := msg.Client() - if msg.target.IsChannel() { - channel := server.channels.Get(msg.target) - if channel == nil { - client.ErrNoSuchChannel(msg.target) - return +// PRIVMSG {,} +func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + targets := strings.Split(msg.Params[0], ",") + message := msg.Params[1] + + for _, target := range targets { + if target.IsChannel() { + channel := server.channels.Get(target) + if channel == nil { + client.Send("send") + client.ErrNoSuchChannel(target) + continue + } + channel.PrivMsg(client, message) + } else { + user := server.clients.Get(target) + if user == nil { + client.Send("send") + client.ErrNoSuchNick(target) + return + } + user.Send("content here") + if user.flags[Away] { + client.Send("target is AWAY") + } } - - channel.PrivMsg(client, msg.message) - return - } - - target := server.clients.Get(msg.target) - if target == nil { - client.ErrNoSuchNick(msg.target) - return - } - target.Reply(RplPrivMsg(client, target, msg.message)) - if target.flags[Away] { - client.RplAway(target) } } @@ -541,19 +585,29 @@ func (client *Client) WhoisChannelsNames(target *Client) []string { return chstrs } -func (m *WhoisCommand) HandleServer(server *Server) { - client := m.Client() +// WHOIS [ ] *( "," ) +func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var masks string + var target string + + if len(msg.Params) > 1 { + target = msg.Params[0] + masks = msg.Params[1] + } else { + masks = msg.Params[0] + } // TODO implement target query - - for _, mask := range m.masks { + for _, mask := range masks { matches := server.clients.FindAll(mask) if len(matches) == 0 { client.ErrNoSuchNick(mask) + client.Send("NOSUCHNICK") continue } for mclient := range matches { client.RplWhois(mclient) + client.Send("WHOIS") } } } @@ -561,15 +615,27 @@ func (m *WhoisCommand) HandleServer(server *Server) { func whoChannel(client *Client, channel *Channel, friends ClientSet) { for member := range channel.members { if !client.flags[Invisible] || friends[client] { + client.Send("send") client.RplWhoReply(channel, member) } } } -func (msg *WhoCommand) HandleServer(server *Server) { - client := msg.Client() +// WHO [ [ "o" ] ] +func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { friends := client.Friends() - mask := msg.mask + + var mask string + if len(msg.Params) > 0 { + mask = NewName(msg.Params[0]) + } + + //TODO(dan): is this used and would I put this param in the Modern doc? + // if not, can we remove it? + var operatorOnly bool + if len(msg.Params) > 1 && msr.Params[1] == "o" { + operatorOnly = true + } if mask == "" { for _, channel := range server.channels { @@ -584,101 +650,161 @@ func (msg *WhoCommand) HandleServer(server *Server) { } else { for mclient := range server.clients.FindAll(mask) { client.RplWhoReply(nil, mclient) + client.Send("REPLY") } } client.RplEndOfWho(mask) + client.Send("ENDOFWHO") } -func (msg *OperCommand) HandleServer(server *Server) { - client := msg.Client() +// OPER +func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + name = NewName(msg.Params[0]) + hash = server.operators[name] + password = []byte(msg.Params[1]) - if (msg.hash == nil) || (msg.err != nil) { + err = ComparePassword(hash, password) + + if (hash == nil) || (err != nil) { client.ErrPasswdMismatch() - return + client.Send("PASSWDBAD") + return true } + //TODO(dan): Split this into client.makeOper() ?? client.flags[Operator] = true client.RplYoureOper() + client.Send("YOUROPER") client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{ mode: Operator, op: Add, }})) + client.Send("OPERMODECHANGE") } -func (msg *AwayCommand) HandleServer(server *Server) { - client := msg.Client() - if len(msg.text) > 0 { +// AWAY [] +func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var isAway bool + var text string + if len(msg.Params) > 0 { + isAway = True + text = NewText(msg.Params[0]) + } + + if isAway { client.flags[Away] = true } else { delete(client.flags, Away) } - client.awayMessage = msg.text + client.awayMessage = text var op ModeOp if client.flags[Away] { op = Add + client.Send("imaway") client.RplNowAway() } else { op = Remove + client.Send("unaway") client.RplUnAway() } + client.Send("mode changes I guess?") client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{ mode: Away, op: op, }})) } -func (msg *IsOnCommand) HandleServer(server *Server) { - client := msg.Client() +// ISON { } +func isonHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var nicks = NewNames(msg.Params) ison := make([]string, 0) - for _, nick := range msg.nicks { + for _, nick := range nicks { if iclient := server.clients.Get(nick); iclient != nil { ison = append(ison, iclient.Nick().String()) } } + client.Send("ISON") client.RplIsOn(ison) } -func (msg *MOTDCommand) HandleServer(server *Server) { +// MOTD [] +func motdHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + //TODO(dan): hook this up when we have multiple servers I guess??? + var target string + if len(msg.Params) > 0 { + target = NewName(msg.Params[0]) + } + + client.Send("MOTD") server.MOTD(msg.Client()) } -func (msg *NoticeCommand) HandleServer(server *Server) { - client := msg.Client() - if msg.target.IsChannel() { - channel := server.channels.Get(msg.target) +// NOTICE {,} +func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + targetName := NewName(msg.Params[0]) + message := NewText(msg.Params[1]) + + if targetName.IsChannel() { + channel := server.channels.Get(targetName) if channel == nil { - client.ErrNoSuchChannel(msg.target) + client.Send("ERRNOSUCHCHAN") + client.ErrNoSuchChannel(targetName) return } - channel.Notice(client, msg.message) + channel.Notice(client, message) return } - target := server.clients.Get(msg.target) + target := server.clients.Get(targetName) if target == nil { - client.ErrNoSuchNick(msg.target) + client.Send("ERRNOSUCHNICK") + client.ErrNoSuchNick(targetName) return } - target.Reply(RplNotice(client, target, msg.message)) + client.Send("NOTICE") + target.Reply(RplNotice(client, target, message)) } -func (msg *KickCommand) HandleServer(server *Server) { - client := msg.Client() - for chname, nickname := range msg.kicks { +// KICK {,} {,} [] +func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + channels := NewNames(strings.Split(msg.Params[0], ",")) + users := NewNames(strings.Split(msg.Params[1], ",")) + if (len(channels) != len(users)) && (len(users) != 1) { + client.Send("NotEnoughArgs??") + return false + // not needed return nil, NotEnoughArgsError + } + + kicks := make(map[Name]Name) + for index, channel := range channels { + if len(users) == 1 { + kicks[channel] = users[0] + } else { + kicks[channel] = users[index] + } + } + + var comment string + if len(msg.Params) > 2 { + comment = msg.Params[2] + } + for chname, nickname := range kicks { channel := server.channels.Get(chname) if channel == nil { client.ErrNoSuchChannel(chname) + client.Send("send") continue } target := server.clients.Get(nickname) if target == nil { client.ErrNoSuchNick(nickname) + client.Send("send") continue } @@ -700,128 +826,196 @@ func (msg *KickCommand) HandleServer(server *Server) { } if hasPrivs { - channel.Kick(client, target, msg.Comment()) + if comment == "" { + channel.Kick(client, target, nickname) + } else { + channel.Kick(client, target, comment) + } } else { client.ErrChanOPrivIsNeeded(channel) + client.Send("send") } } } -func (msg *ListCommand) HandleServer(server *Server) { - client := msg.Client() +// LIST [{,} []] +func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var channels []Name + if len(args) > 0 { + channels = NewNames(strings.Split(args[0], ",")) + } + var target Name + if len(args) > 1 { + target = NewName(args[1]) + } - // TODO target server - if msg.target != "" { + //TODO(dan): target server when we have multiple servers + //TODO(dan): we should continue just fine if it's this current server though + if target != "" { client.ErrNoSuchServer(msg.target) + client.Send("send") return } - if len(msg.channels) == 0 { + if len(channels) == 0 { for _, channel := range server.channels { if !client.flags[Operator] && channel.flags[Secret] { continue } client.RplList(channel) + client.Send("send") } } else { - for _, chname := range msg.channels { + for _, chname := range channels { channel := server.channels.Get(chname) if channel == nil || (!client.flags[Operator] && channel.flags[Secret]) { client.ErrNoSuchChannel(chname) + client.Send("send") continue } client.RplList(channel) + client.Send("send") } } client.RplListEnd(server) + client.Send("send") } -func (msg *NamesCommand) HandleServer(server *Server) { - client := msg.Client() - if len(server.channels) == 0 { +// NAMES [{,}] +func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var channels []Name + if len(args) > 0 { + channels = NewNames(strings.Split(args[0], ",")) + } + var target Name + if len(args) > 1 { + target = NewName(args[1]) + } + + if len(channels) == 0 { for _, channel := range server.channels { channel.Names(client) } - return + return false } - for _, chname := range msg.channels { + for _, chname := range channels { channel := server.channels.Get(chname) if channel == nil { client.ErrNoSuchChannel(chname) + client.Send("send") continue } channel.Names(client) + client.Send("send") } } -func (msg *VersionCommand) HandleServer(server *Server) { - client := msg.Client() - if (msg.target != "") && (msg.target != server.name) { - client.ErrNoSuchServer(msg.target) +// VERSION [] +func versionHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var target Name + if len(args) > 0 { + target = NewName(args[0]) + } + if (target != "") && (target != server.name) { + client.ErrNoSuchServer(target) + client.Send("send") return } client.RplVersion() + client.Send("send") client.RplISupport() + client.Send("send") } -func (msg *InviteCommand) HandleServer(server *Server) { - client := msg.Client() +// INVITE +func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + nickname := NewName(msg.Params[0]) + channelName := NewName(msg.Params[1]) - target := server.clients.Get(msg.nickname) + target := server.clients.Get(nickname) if target == nil { - client.ErrNoSuchNick(msg.nickname) + client.ErrNoSuchNick(nickname) + client.Send("send") return } - channel := server.channels.Get(msg.channel) + channel := server.channels.Get(channelName) if channel == nil { - client.RplInviting(target, msg.channel) - target.Reply(RplInviteMsg(client, target, msg.channel)) + client.RplInviting(target, channelName) + client.Send("send") + target.Reply(RplInviteMsg(client, target, channelName)) + client.Send("send") return } channel.Invite(target, client) } -func (msg *TimeCommand) HandleServer(server *Server) { - client := msg.Client() - if (msg.target != "") && (msg.target != server.name) { - client.ErrNoSuchServer(msg.target) +// TIME [] +func timeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + var target Name + if len(msg.Params) > 0 { + target = NewName(msg.Params[0]) + } + if (target != "") && (target != server.name) { + client.ErrNoSuchServer(target) + client.Send("send") return } client.RplTime() + client.Send("send") } -func (msg *KillCommand) HandleServer(server *Server) { - client := msg.Client() +// KILL +func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + nickname := NewName(msg.Params[0]) + comment := NewText(msg.Params[1]) + if !client.flags[Operator] { client.ErrNoPrivileges() + client.Send("send") return } - target := server.clients.Get(msg.nickname) + target := server.clients.Get(nickname) if target == nil { - client.ErrNoSuchNick(msg.nickname) + client.ErrNoSuchNick(nickname) + client.Send("send") return } - quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), msg.comment) + //TODO(dan): make below format match that from other IRCds + quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), comment) target.Quit(NewText(quitMsg)) + return true } -func (msg *WhoWasCommand) HandleServer(server *Server) { - client := msg.Client() - for _, nickname := range msg.nicknames { +// WHOWAS [ []] +func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + nicknames := NewNames(strings.Split(msg.Params[0], ",")) + + var count int + if len(msg.Params) > 1 { + count, _ = strconv.ParseInt(msg.Params[1], 10, 64) + } + var target Name + if len(msg.Params) > 2 { + target = NewName(msg.Params[2]) + } + for _, nickname := range nicknames { results := server.whoWas.Find(nickname, msg.count) if len(results) == 0 { client.ErrWasNoSuchNick(nickname) + client.Send("send") } else { for _, whoWas := range results { client.RplWhoWasUser(whoWas) + client.Send("send") } } client.RplEndOfWhoWas(nickname) + client.Send("send") } }