diff --git a/README.md b/README.md index f88d3754..5bd1704e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ hostname lookups. ```sh go get go install -ergonomadic -conf ergonomadic.conf -initdb +ergonomadic initdb -conf ergonomadic.conf ``` ## Configuration @@ -48,16 +48,16 @@ bcrypted byte strings. You can generate them with the `genpasswd` subcommand. ```sh -ergonomadic -genpasswd 'hunter2!' +ergonomadic genpasswd 'hunter2!' ``` ## Running the Server ```sh -ergonomadic -conf ergonomadic.conf +ergonomadic run -conf ergonomadic.conf ``` -## Helpful Documentation +## IRC Documentation - [RFC 1459: Internet Relay Chat Protocol](http://tools.ietf.org/html/rfc1459) - [RFC 2811: IRC Channel Management](http://tools.ietf.org/html/rfc2811) diff --git a/ergonomadic.conf b/ergonomadic.conf index 2f360024..c638d3dd 100644 --- a/ergonomadic.conf +++ b/ergonomadic.conf @@ -3,14 +3,9 @@ name = "irc.example.com" ; required, usually a hostname database = "ergonomadic.db" ; path relative to this file listen = "localhost:6667" ; see `net.Listen` for examples listen = "[::1]:6667" ; multiple `listen`s are allowed. +log = "debug" ; error, warn, info, debug motd = "motd.txt" ; path relative to this file password = "JDJhJDA0JHJzVFFlNXdOUXNhLmtkSGRUQVVEVHVYWXRKUmdNQ3FKVTRrczRSMTlSWGRPZHRSMVRzQmtt" ; 'test' [operator "root"] password = "JDJhJDA0JEhkcm10UlNFRkRXb25iOHZuSDVLZXVBWlpyY0xyNkQ4dlBVc1VMWVk1LlFjWFpQbGxZNUtl" ; 'toor' - -[debug] -net = true -client = false -channel = false -server = false diff --git a/ergonomadic.go b/ergonomadic.go index 3d107a7c..11239ad6 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -9,51 +9,68 @@ import ( "path/filepath" ) +func usage() { + fmt.Fprintln(os.Stderr, "ergonomadic [options]") + fmt.Fprintln(os.Stderr, " run -conf -- run server") + fmt.Fprintln(os.Stderr, " initdb -conf -- initialize database") + fmt.Fprintln(os.Stderr, " upgrade -conf -- upgrade database") + fmt.Fprintln(os.Stderr, " genpasswd -- bcrypt a password") + flag.PrintDefaults() +} + +func loadConfig(conf string) *irc.Config { + config, err := irc.LoadConfig(conf) + if err != nil { + log.Fatalln("error loading config:", err) + } + + err = os.Chdir(filepath.Dir(conf)) + if err != nil { + log.Fatalln("chdir error:", err) + } + return config +} + +func genPasswd() { +} + func main() { - conf := flag.String("conf", "ergonomadic.conf", "ergonomadic config file") - initdb := flag.Bool("initdb", false, "initialize database") - upgradedb := flag.Bool("upgradedb", false, "update database") - passwd := flag.String("genpasswd", "", "bcrypt a password") + var conf string + flag.Usage = usage + + runFlags := flag.NewFlagSet("run", flag.ExitOnError) + runFlags.Usage = usage + runFlags.StringVar(&conf, "conf", "ergonomadic.conf", "ergonomadic config file") + flag.Parse() - if *passwd != "" { - encoded, err := irc.GenerateEncodedPassword(*passwd) + switch flag.Arg(0) { + case "genpasswd": + encoded, err := irc.GenerateEncodedPassword(flag.Arg(1)) if err != nil { - log.Fatal("encoding error: ", err) + log.Fatalln("encoding error:", err) } fmt.Println(encoded) - return - } - config, err := irc.LoadConfig(*conf) - if err != nil { - log.Fatal("error loading config: ", err) - } - err = os.Chdir(filepath.Dir(*conf)) - if err != nil { - log.Fatal("chdir error: ", err) - } - - if *initdb { + case "initdb": + runFlags.Parse(flag.Args()[1:]) + config := loadConfig(conf) irc.InitDB(config.Server.Database) log.Println("database initialized: ", config.Server.Database) - return - } - if *upgradedb { + case "upgradedb": + runFlags.Parse(flag.Args()[1:]) + config := loadConfig(conf) irc.UpgradeDB(config.Server.Database) log.Println("database upgraded: ", config.Server.Database) - return + + default: + runFlags.Parse(flag.Args()[1:]) + config := loadConfig(conf) + irc.Log.SetLevel(config.Server.Log) + server := irc.NewServer(config) + log.Println(irc.SEM_VER, "running") + defer log.Println(irc.SEM_VER, "exiting") + server.Run() } - - // TODO move to data structures - irc.DEBUG_NET = config.Debug.Net - irc.DEBUG_CLIENT = config.Debug.Client - irc.DEBUG_CHANNEL = config.Debug.Channel - irc.DEBUG_SERVER = config.Debug.Server - - server := irc.NewServer(config) - log.Println(irc.SEM_VER, "running") - defer log.Println(irc.SEM_VER, "exiting") - server.Run() } diff --git a/irc/capability.go b/irc/capability.go new file mode 100644 index 00000000..3ec4d3f9 --- /dev/null +++ b/irc/capability.go @@ -0,0 +1,116 @@ +package irc + +import ( + "strings" +) + +type CapSubCommand string + +const ( + CAP_LS CapSubCommand = "LS" + CAP_LIST CapSubCommand = "LIST" + CAP_REQ CapSubCommand = "REQ" + CAP_ACK CapSubCommand = "ACK" + CAP_NAK CapSubCommand = "NAK" + CAP_CLEAR CapSubCommand = "CLEAR" + CAP_END CapSubCommand = "END" +) + +// Capabilities are optional features a client may request from a server. +type Capability string + +const ( + MultiPrefix Capability = "multi-prefix" + SASL Capability = "sasl" +) + +var ( + SupportedCapabilities = CapabilitySet{ + MultiPrefix: true, + } +) + +func (capability Capability) String() string { + return string(capability) +} + +// CapModifiers are indicators showing the state of a capability after a REQ or +// ACK. +type CapModifier rune + +const ( + Ack CapModifier = '~' + Disable CapModifier = '-' + Sticky CapModifier = '=' +) + +func (mod CapModifier) String() string { + return string(mod) +} + +type CapState uint + +const ( + CapNone CapState = iota + CapNegotiating CapState = iota + CapNegotiated CapState = iota +) + +type CapabilitySet map[Capability]bool + +func (set CapabilitySet) String() string { + strs := make([]string, len(set)) + index := 0 + for capability := range set { + strs[index] = string(capability) + index += 1 + } + return strings.Join(strs, " ") +} + +func (set CapabilitySet) DisableString() string { + parts := make([]string, len(set)) + index := 0 + for capability := range set { + parts[index] = Disable.String() + capability.String() + index += 1 + } + return strings.Join(parts, " ") +} + +func (msg *CapCommand) HandleRegServer(server *Server) { + client := msg.Client() + + switch msg.subCommand { + case CAP_LS: + client.capState = CapNegotiating + client.Reply(RplCap(client, CAP_LS, SupportedCapabilities)) + + case CAP_LIST: + client.Reply(RplCap(client, CAP_LIST, client.capabilities)) + + case CAP_REQ: + for capability := range msg.capabilities { + if !SupportedCapabilities[capability] { + client.Reply(RplCap(client, CAP_NAK, msg.capabilities)) + return + } + } + for capability := range msg.capabilities { + client.capabilities[capability] = true + } + client.Reply(RplCap(client, CAP_ACK, msg.capabilities)) + + case CAP_CLEAR: + reply := RplCap(client, CAP_ACK, client.capabilities.DisableString()) + client.capabilities = make(CapabilitySet) + client.Reply(reply) + + case CAP_END: + client.capState = CapNegotiated + server.tryRegister(client) + + default: + client.ErrInvalidCapCmd(msg.subCommand) + } +} diff --git a/irc/channel.go b/irc/channel.go index d985d6f7..07b52913 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -444,8 +444,8 @@ func (channel *Channel) Persist() (err error) { (name, flags, key, topic, user_limit, ban_list, except_list, invite_list) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - channel.name, channel.flags.String(), channel.key, channel.topic, - channel.userLimit, channel.lists[BanMask].String(), + channel.name.String(), channel.flags.String(), channel.key.String(), + channel.topic.String(), channel.userLimit, channel.lists[BanMask].String(), channel.lists[ExceptMask].String(), channel.lists[InviteMask].String()) } else { _, err = channel.server.db.Exec(` diff --git a/irc/client.go b/irc/client.go index a35f214d..ec4c323d 100644 --- a/irc/client.go +++ b/irc/client.go @@ -2,11 +2,16 @@ package irc import ( "fmt" - "log" "net" "time" ) +const ( + LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login + IDLE_TIMEOUT = time.Minute // how long before a client is considered idle + QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked +) + type Client struct { atime time.Time authorized bool @@ -14,7 +19,7 @@ type Client struct { capabilities CapabilitySet capState CapState channels ChannelSet - commands chan editableCommand + commands chan Command ctime time.Time flags map[UserMode]bool hasQuit bool @@ -23,9 +28,9 @@ type Client struct { idleTimer *time.Timer loginTimer *time.Timer nick Name - phase Phase quitTimer *time.Timer realname Text + registered bool server *Server socket *Socket username Name @@ -39,10 +44,9 @@ func NewClient(server *Server, conn net.Conn) *Client { capState: CapNone, capabilities: make(CapabilitySet), channels: make(ChannelSet), - commands: make(chan editableCommand), + commands: make(chan Command), ctime: now, flags: make(map[UserMode]bool), - phase: Registration, server: server, } client.socket = NewSocket(conn, client.commands) @@ -115,7 +119,10 @@ func (client *Client) Idle() { } func (client *Client) Register() { - client.phase = Normal + if client.registered { + return + } + client.registered = true client.loginTimer.Stop() client.Touch() } @@ -145,9 +152,7 @@ func (client *Client) destroy() { client.socket.Close() - if DEBUG_CLIENT { - log.Printf("%s: destroyed", client) - } + Log.debug.Printf("%s: destroyed", client) } func (client *Client) IdleTime() time.Duration { diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 703f541e..844e3f3e 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -88,25 +88,19 @@ func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) { `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`, QuoteLike(userhost)) if err != nil { - if DEBUG_SERVER { - log.Println("ClientLookupSet.FindAll.Query:", err) - } + Log.error.Println("ClientLookupSet.FindAll.Query:", err) return } for rows.Next() { var nickname Name err := rows.Scan(&nickname) if err != nil { - if DEBUG_SERVER { - log.Println("ClientLookupSet.FindAll.Scan:", err) - } + Log.error.Println("ClientLookupSet.FindAll.Scan:", err) return } client := clients.Get(nickname) if client == nil { - if DEBUG_SERVER { - log.Println("ClientLookupSet.FindAll: missing client:", nickname) - } + Log.error.Println("ClientLookupSet.FindAll: missing client:", nickname) continue } set.Add(client) @@ -122,9 +116,7 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client { var nickname Name err := row.Scan(&nickname) if err != nil { - if DEBUG_SERVER { - log.Println("ClientLookupSet.Find:", err) - } + Log.error.Println("ClientLookupSet.Find:", err) return nil } return clients.Get(nickname) @@ -161,21 +153,17 @@ func NewClientDB() *ClientDB { func (db *ClientDB) Add(client *Client) { _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, - client.Nick(), client.UserHost()) + client.Nick().String(), client.UserHost().String()) if err != nil { - if DEBUG_SERVER { - log.Println("ClientDB.Add:", err) - } + Log.error.Println("ClientDB.Add:", err) } } func (db *ClientDB) Remove(client *Client) { _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, - client.Nick()) + client.Nick().String()) if err != nil { - if DEBUG_SERVER { - log.Println("ClientDB.Remove:", err) - } + Log.error.Println("ClientDB.Remove:", err) } } diff --git a/irc/commands.go b/irc/commands.go index dca13a7b..7601969b 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -8,10 +8,11 @@ import ( "strings" ) -type editableCommand interface { - Command - SetCode(StringCode) +type Command interface { + Client() *Client + Code() StringCode SetClient(*Client) + SetCode(StringCode) } type checkPasswordCommand interface { @@ -19,7 +20,7 @@ type checkPasswordCommand interface { CheckPassword() } -type parseCommandFunc func([]string) (editableCommand, error) +type parseCommandFunc func([]string) (Command, error) var ( NotEnoughArgsError = errors.New("not enough arguments") @@ -78,7 +79,7 @@ func (command *BaseCommand) SetCode(code StringCode) { command.code = code } -func ParseCommand(line string) (cmd editableCommand, err error) { +func ParseCommand(line string) (cmd Command, err error) { code, args := ParseLine(line) constructor := parseCommandFuncs[code] if constructor == nil { @@ -154,7 +155,7 @@ func (cmd *PingCommand) String() string { return fmt.Sprintf("PING(server=%s, server2=%s)", cmd.server, cmd.server2) } -func NewPingCommand(args []string) (editableCommand, error) { +func NewPingCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -179,7 +180,7 @@ func (cmd *PongCommand) String() string { return fmt.Sprintf("PONG(server1=%s, server2=%s)", cmd.server1, cmd.server2) } -func NewPongCommand(args []string) (editableCommand, error) { +func NewPongCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -216,7 +217,7 @@ func (cmd *PassCommand) CheckPassword() { cmd.err = ComparePassword(cmd.hash, cmd.password) } -func NewPassCommand(args []string) (editableCommand, error) { +func NewPassCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -236,7 +237,7 @@ func (m *NickCommand) String() string { return fmt.Sprintf("NICK(nickname=%s)", m.nickname) } -func NewNickCommand(args []string) (editableCommand, error) { +func NewNickCommand(args []string) (Command, error) { if len(args) != 1 { return nil, NotEnoughArgsError } @@ -286,7 +287,7 @@ func (cmd *RFC2812UserCommand) Flags() []UserMode { return flags } -func NewUserCommand(args []string) (editableCommand, error) { +func NewUserCommand(args []string) (Command, error) { if len(args) != 4 { return nil, NotEnoughArgsError } @@ -321,7 +322,7 @@ func (cmd *QuitCommand) String() string { return fmt.Sprintf("QUIT(message=%s)", cmd.message) } -func NewQuitCommand(args []string) (editableCommand, error) { +func NewQuitCommand(args []string) (Command, error) { msg := &QuitCommand{} if len(args) > 0 { msg.message = NewText(args[0]) @@ -341,7 +342,7 @@ func (cmd *JoinCommand) String() string { return fmt.Sprintf("JOIN(channels=%s, zero=%t)", cmd.channels, cmd.zero) } -func NewJoinCommand(args []string) (editableCommand, error) { +func NewJoinCommand(args []string) (Command, error) { msg := &JoinCommand{ channels: make(map[Name]Text), } @@ -388,7 +389,7 @@ func (cmd *PartCommand) String() string { return fmt.Sprintf("PART(channels=%s, message=%s)", cmd.channels, cmd.message) } -func NewPartCommand(args []string) (editableCommand, error) { +func NewPartCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -413,7 +414,7 @@ func (cmd *PrivMsgCommand) String() string { return fmt.Sprintf("PRIVMSG(target=%s, message=%s)", cmd.target, cmd.message) } -func NewPrivMsgCommand(args []string) (editableCommand, error) { +func NewPrivMsgCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -436,7 +437,7 @@ func (cmd *TopicCommand) String() string { return fmt.Sprintf("TOPIC(channel=%s, topic=%s)", cmd.channel, cmd.topic) } -func NewTopicCommand(args []string) (editableCommand, error) { +func NewTopicCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -486,7 +487,7 @@ type ModeCommand struct { } // MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) -func NewUserModeCommand(nickname Name, args []string) (editableCommand, error) { +func NewUserModeCommand(nickname Name, args []string) (Command, error) { cmd := &ModeCommand{ nickname: nickname, changes: make(ModeChanges, 0), @@ -563,7 +564,7 @@ type ChannelModeCommand struct { } // MODE *( ( "-" / "+" ) * * ) -func NewChannelModeCommand(channel Name, args []string) (editableCommand, error) { +func NewChannelModeCommand(channel Name, args []string) (Command, error) { cmd := &ChannelModeCommand{ channel: channel, changes: make(ChannelModeChanges, 0), @@ -609,7 +610,7 @@ func (msg *ChannelModeCommand) String() string { return fmt.Sprintf("MODE(channel=%s, changes=%s)", msg.channel, msg.changes) } -func NewModeCommand(args []string) (editableCommand, error) { +func NewModeCommand(args []string) (Command, error) { if len(args) == 0 { return nil, NotEnoughArgsError } @@ -629,7 +630,7 @@ type WhoisCommand struct { } // WHOIS [ ] *( "," ) -func NewWhoisCommand(args []string) (editableCommand, error) { +func NewWhoisCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -661,7 +662,7 @@ type WhoCommand struct { } // WHO [ [ "o" ] ] -func NewWhoCommand(args []string) (editableCommand, error) { +func NewWhoCommand(args []string) (Command, error) { cmd := &WhoCommand{} if len(args) > 0 { @@ -693,7 +694,7 @@ func (msg *OperCommand) LoadPassword(server *Server) { } // OPER -func NewOperCommand(args []string) (editableCommand, error) { +func NewOperCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -716,7 +717,7 @@ func (msg *CapCommand) String() string { msg.subCommand, msg.capabilities) } -func NewCapCommand(args []string) (editableCommand, error) { +func NewCapCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -750,7 +751,7 @@ func (msg *ProxyCommand) String() string { return fmt.Sprintf("PROXY(sourceIP=%s, sourcePort=%s)", msg.sourceIP, msg.sourcePort) } -func NewProxyCommand(args []string) (editableCommand, error) { +func NewProxyCommand(args []string) (Command, error) { if len(args) < 5 { return nil, NotEnoughArgsError } @@ -774,7 +775,7 @@ func (msg *AwayCommand) String() string { return fmt.Sprintf("AWAY(%s)", msg.text) } -func NewAwayCommand(args []string) (editableCommand, error) { +func NewAwayCommand(args []string) (Command, error) { cmd := &AwayCommand{} if len(args) > 0 { @@ -794,7 +795,7 @@ func (msg *IsOnCommand) String() string { return fmt.Sprintf("ISON(nicks=%s)", msg.nicks) } -func NewIsOnCommand(args []string) (editableCommand, error) { +func NewIsOnCommand(args []string) (Command, error) { if len(args) == 0 { return nil, NotEnoughArgsError } @@ -809,7 +810,7 @@ type MOTDCommand struct { target Name } -func NewMOTDCommand(args []string) (editableCommand, error) { +func NewMOTDCommand(args []string) (Command, error) { cmd := &MOTDCommand{} if len(args) > 0 { cmd.target = NewName(args[0]) @@ -827,7 +828,7 @@ func (cmd *NoticeCommand) String() string { return fmt.Sprintf("NOTICE(target=%s, message=%s)", cmd.target, cmd.message) } -func NewNoticeCommand(args []string) (editableCommand, error) { +func NewNoticeCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -850,7 +851,7 @@ func (msg *KickCommand) Comment() Text { return msg.comment } -func NewKickCommand(args []string) (editableCommand, error) { +func NewKickCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -881,7 +882,7 @@ type ListCommand struct { target Name } -func NewListCommand(args []string) (editableCommand, error) { +func NewListCommand(args []string) (Command, error) { cmd := &ListCommand{} if len(args) > 0 { cmd.channels = NewNames(strings.Split(args[0], ",")) @@ -898,7 +899,7 @@ type NamesCommand struct { target Name } -func NewNamesCommand(args []string) (editableCommand, error) { +func NewNamesCommand(args []string) (Command, error) { cmd := &NamesCommand{} if len(args) > 0 { cmd.channels = NewNames(strings.Split(args[0], ",")) @@ -914,7 +915,7 @@ type DebugCommand struct { subCommand Name } -func NewDebugCommand(args []string) (editableCommand, error) { +func NewDebugCommand(args []string) (Command, error) { if len(args) == 0 { return nil, NotEnoughArgsError } @@ -929,7 +930,7 @@ type VersionCommand struct { target Name } -func NewVersionCommand(args []string) (editableCommand, error) { +func NewVersionCommand(args []string) (Command, error) { cmd := &VersionCommand{} if len(args) > 0 { cmd.target = NewName(args[0]) @@ -943,7 +944,7 @@ type InviteCommand struct { channel Name } -func NewInviteCommand(args []string) (editableCommand, error) { +func NewInviteCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -959,7 +960,7 @@ type TimeCommand struct { target Name } -func NewTimeCommand(args []string) (editableCommand, error) { +func NewTimeCommand(args []string) (Command, error) { cmd := &TimeCommand{} if len(args) > 0 { cmd.target = NewName(args[0]) @@ -973,7 +974,7 @@ type KillCommand struct { comment Text } -func NewKillCommand(args []string) (editableCommand, error) { +func NewKillCommand(args []string) (Command, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -990,7 +991,7 @@ type WhoWasCommand struct { target Name } -func NewWhoWasCommand(args []string) (editableCommand, error) { +func NewWhoWasCommand(args []string) (Command, error) { if len(args) < 1 { return nil, NotEnoughArgsError } diff --git a/irc/config.go b/irc/config.go index 480394d0..69bec631 100644 --- a/irc/config.go +++ b/irc/config.go @@ -23,18 +23,12 @@ type Config struct { PassConfig Database string Listen []string + Log string MOTD string Name string } Operator map[string]*PassConfig - - Debug struct { - Net bool - Client bool - Channel bool - Server bool - } } func (conf *Config) Operators() map[Name][]byte { diff --git a/irc/constants.go b/irc/constants.go index 9a3252b9..d9426cb8 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -1,30 +1,10 @@ package irc -import ( - "errors" - "time" -) - -var ( - // debugging flags - DEBUG_NET = false - DEBUG_CLIENT = false - DEBUG_CHANNEL = false - DEBUG_SERVER = false - - // errors - ErrAlreadyDestroyed = errors.New("already destroyed") -) - const ( SEM_VER = "ergonomadic-1.3.1" CRLF = "\r\n" MAX_REPLY_LEN = 512 - len(CRLF) - LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login - IDLE_TIMEOUT = time.Minute // how long before a client is considered idle - QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked - // string codes AWAY StringCode = "AWAY" CAP StringCode = "CAP" @@ -195,67 +175,4 @@ const ( ERR_NOOPERHOST NumericCode = 491 ERR_UMODEUNKNOWNFLAG NumericCode = 501 ERR_USERSDONTMATCH NumericCode = 502 - - CAP_LS CapSubCommand = "LS" - CAP_LIST CapSubCommand = "LIST" - CAP_REQ CapSubCommand = "REQ" - CAP_ACK CapSubCommand = "ACK" - CAP_NAK CapSubCommand = "NAK" - CAP_CLEAR CapSubCommand = "CLEAR" - CAP_END CapSubCommand = "END" - - Add ModeOp = '+' - List ModeOp = '=' - Remove ModeOp = '-' - - Away UserMode = 'a' - Invisible UserMode = 'i' - LocalOperator UserMode = 'O' - Operator UserMode = 'o' - Restricted UserMode = 'r' - ServerNotice UserMode = 's' // deprecated - WallOps UserMode = 'w' - - Anonymous ChannelMode = 'a' // flag - BanMask ChannelMode = 'b' // arg - ChannelCreator ChannelMode = 'O' // flag - ChannelOperator ChannelMode = 'o' // arg - ExceptMask ChannelMode = 'e' // arg - InviteMask ChannelMode = 'I' // arg - InviteOnly ChannelMode = 'i' // flag - Key ChannelMode = 'k' // flag arg - Moderated ChannelMode = 'm' // flag - NoOutside ChannelMode = 'n' // flag - OpOnlyTopic ChannelMode = 't' // flag - Persistent ChannelMode = 'P' // flag - Private ChannelMode = 'p' // flag - Quiet ChannelMode = 'q' // flag - ReOp ChannelMode = 'r' // flag - Secret ChannelMode = 's' // flag, deprecated - UserLimit ChannelMode = 'l' // flag arg - Voice ChannelMode = 'v' // arg - - MultiPrefix Capability = "multi-prefix" - SASL Capability = "sasl" - - Disable CapModifier = '-' - Ack CapModifier = '~' - Sticky CapModifier = '=' -) - -var ( - SupportedCapabilities = CapabilitySet{ - MultiPrefix: true, - } -) - -const ( - Registration Phase = iota - Normal Phase = iota -) - -const ( - CapNone CapState = iota - CapNegotiating CapState = iota - CapNegotiated CapState = iota ) diff --git a/irc/logging.go b/irc/logging.go new file mode 100644 index 00000000..6390aab2 --- /dev/null +++ b/irc/logging.go @@ -0,0 +1,60 @@ +package irc + +import ( + "io" + "log" + "os" +) + +type Logging struct { + debug *log.Logger + info *log.Logger + warn *log.Logger + error *log.Logger +} + +var ( + levels = map[string]uint8{ + "debug": 4, + "info": 3, + "warn": 2, + "error": 1, + } + devNull io.Writer +) + +func init() { + var err error + devNull, err = os.Open(os.DevNull) + if err != nil { + log.Fatal(err) + } +} + +func NewLogger(on bool) *log.Logger { + return log.New(output(on), "", log.LstdFlags) +} + +func output(on bool) io.Writer { + if on { + return os.Stdout + } + return devNull +} + +func (logging *Logging) SetLevel(level string) { + logging.debug = NewLogger(levels[level] >= levels["debug"]) + logging.info = NewLogger(levels[level] >= levels["info"]) + logging.warn = NewLogger(levels[level] >= levels["warn"]) + logging.error = NewLogger(levels[level] >= levels["error"]) +} + +func NewLogging(level string) *Logging { + logging := &Logging{} + logging.SetLevel(level) + return logging +} + +var ( + Log = NewLogging("warn") +) diff --git a/irc/modes.go b/irc/modes.go new file mode 100644 index 00000000..7dfb3c08 --- /dev/null +++ b/irc/modes.go @@ -0,0 +1,162 @@ +package irc + +import ( + "strings" +) + +// user mode flags +type UserMode rune + +func (mode UserMode) String() string { + return string(mode) +} + +type UserModes []UserMode + +func (modes UserModes) String() string { + strs := make([]string, len(modes)) + for index, mode := range modes { + strs[index] = mode.String() + } + return strings.Join(strs, "") +} + +// channel mode flags +type ChannelMode rune + +func (mode ChannelMode) String() string { + return string(mode) +} + +type ChannelModes []ChannelMode + +func (modes ChannelModes) String() string { + strs := make([]string, len(modes)) + for index, mode := range modes { + strs[index] = mode.String() + } + return strings.Join(strs, "") +} + +type ModeOp rune + +func (op ModeOp) String() string { + return string(op) +} + +const ( + Add ModeOp = '+' + List ModeOp = '=' + Remove ModeOp = '-' +) + +const ( + Away UserMode = 'a' + Invisible UserMode = 'i' + LocalOperator UserMode = 'O' + Operator UserMode = 'o' + Restricted UserMode = 'r' + ServerNotice UserMode = 's' // deprecated + WallOps UserMode = 'w' +) + +var ( + SupportedUserModes = UserModes{ + Away, Invisible, Operator, + } +) + +const ( + Anonymous ChannelMode = 'a' // flag + BanMask ChannelMode = 'b' // arg + ChannelCreator ChannelMode = 'O' // flag + ChannelOperator ChannelMode = 'o' // arg + ExceptMask ChannelMode = 'e' // arg + InviteMask ChannelMode = 'I' // arg + InviteOnly ChannelMode = 'i' // flag + Key ChannelMode = 'k' // flag arg + Moderated ChannelMode = 'm' // flag + NoOutside ChannelMode = 'n' // flag + OpOnlyTopic ChannelMode = 't' // flag + Persistent ChannelMode = 'P' // flag + Private ChannelMode = 'p' // flag + Quiet ChannelMode = 'q' // flag + ReOp ChannelMode = 'r' // flag + Secret ChannelMode = 's' // flag, deprecated + UserLimit ChannelMode = 'l' // flag arg + Voice ChannelMode = 'v' // arg +) + +var ( + SupportedChannelModes = ChannelModes{ + BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, + OpOnlyTopic, Persistent, Private, UserLimit, + } +) + +// +// commands +// + +func (m *ModeCommand) HandleServer(s *Server) { + client := m.Client() + target := s.clients.Get(m.nickname) + + if target == nil { + client.ErrNoSuchNick(m.nickname) + return + } + + if client != target && !client.flags[Operator] { + client.ErrUsersDontMatch() + return + } + + changes := make(ModeChanges, 0, len(m.changes)) + + for _, change := range m.changes { + switch change.mode { + case Invisible, ServerNotice, WallOps: + switch change.op { + case Add: + if target.flags[change.mode] { + continue + } + target.flags[change.mode] = true + changes = append(changes, change) + + case Remove: + if !target.flags[change.mode] { + continue + } + delete(target.flags, change.mode) + changes = append(changes, change) + } + + case Operator, LocalOperator: + if change.op == Remove { + if !target.flags[change.mode] { + continue + } + delete(target.flags, change.mode) + changes = append(changes, change) + } + } + } + + // Who should get these replies? + if len(changes) > 0 { + client.Reply(RplMode(client, target, changes)) + } +} + +func (msg *ChannelModeCommand) HandleServer(server *Server) { + client := msg.Client() + channel := server.channels.Get(msg.channel) + if channel == nil { + client.ErrNoSuchChannel(msg.channel) + return + } + + channel.Mode(client, msg.changes) +} diff --git a/irc/reply.go b/irc/reply.go index dfd66915..695ac9e5 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -6,7 +6,23 @@ import ( "time" ) -func NewStringReply(source Identifier, code StringCode, +type ReplyCode interface { + String() string +} + +type StringCode string + +func (code StringCode) String() string { + return string(code) +} + +type NumericCode uint + +func (code NumericCode) String() string { + return fmt.Sprintf("%03d", code) +} + +func NewStringReply(source Identifiable, code StringCode, format string, args ...interface{}) string { var header string if source == nil { @@ -79,15 +95,15 @@ func (target *Client) MultilineReply(names []string, code NumericCode, format st // messaging replies // -func RplPrivMsg(source Identifier, target Identifier, message Text) string { +func RplPrivMsg(source Identifiable, target Identifiable, message Text) string { return NewStringReply(source, PRIVMSG, "%s :%s", target.Nick(), message) } -func RplNotice(source Identifier, target Identifier, message Text) string { +func RplNotice(source Identifiable, target Identifiable, message Text) string { return NewStringReply(source, NOTICE, "%s :%s", target.Nick(), message) } -func RplNick(source Identifier, newNick Name) string { +func RplNick(source Identifiable, newNick Name) string { return NewStringReply(source, NICK, newNick.String()) } @@ -108,11 +124,11 @@ func RplChannelMode(client *Client, channel *Channel, return NewStringReply(client, MODE, "%s %s", channel, changes) } -func RplTopicMsg(source Identifier, channel *Channel) string { +func RplTopicMsg(source Identifiable, channel *Channel) string { return NewStringReply(source, TOPIC, "%s :%s", channel, channel.topic) } -func RplPing(target Identifier) string { +func RplPing(target Identifiable) string { return NewStringReply(nil, PING, ":%s", target.Nick()) } @@ -165,7 +181,8 @@ func (target *Client) RplCreated() { func (target *Client) RplMyInfo() { target.NumericReply(RPL_MYINFO, - "%s %s aiOorsw abeIikmntpqrsl", target.server.name, SEM_VER) + "%s %s %s %s", + target.server.name, SEM_VER, SupportedUserModes, SupportedChannelModes) } func (target *Client) RplUModeIs(client *Client) { diff --git a/irc/server.go b/irc/server.go index dc2256ff..b332b882 100644 --- a/irc/server.go +++ b/irc/server.go @@ -16,6 +16,16 @@ import ( "time" ) +type ServerCommand interface { + Command + HandleServer(*Server) +} + +type RegServerCommand interface { + Command + HandleRegServer(*Server) +} + type Server struct { channels ChannelNameMap clients *ClientLookupSet @@ -32,19 +42,24 @@ type Server struct { whoWas *WhoWasList } +var ( + SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP, + syscall.SIGTERM, syscall.SIGQUIT} +) + func NewServer(config *Config) *Server { server := &Server{ channels: make(ChannelNameMap), clients: NewClientLookupSet(), - commands: make(chan Command, 16), + commands: make(chan Command), ctime: time.Now(), db: OpenDB(config.Server.Database), - idle: make(chan *Client, 16), + idle: make(chan *Client), motdFile: config.Server.MOTD, name: NewName(config.Server.Name), - newConns: make(chan net.Conn, 16), + newConns: make(chan net.Conn), operators: config.Operators(), - signals: make(chan os.Signal, 1), + signals: make(chan os.Signal, len(SERVER_SIGNALS)), whoWas: NewWhoWasList(100), } @@ -58,8 +73,7 @@ func NewServer(config *Config) *Server { go server.listen(addr) } - signal.Notify(server.signals, syscall.SIGINT, syscall.SIGHUP, - syscall.SIGTERM, syscall.SIGQUIT) + signal.Notify(server.signals, SERVER_SIGNALS...) return server } @@ -80,9 +94,7 @@ func (server *Server) loadChannels() { log.Fatal("error loading channels: ", err) } for rows.Next() { - var name Name - var flags string - var key, topic Text + var name, flags, key, topic string var userLimit uint64 var banList, exceptList, inviteList string err = rows.Scan(&name, &flags, &key, &topic, &userLimit, &banList, @@ -92,12 +104,12 @@ func (server *Server) loadChannels() { continue } - channel := NewChannel(server, name) + channel := NewChannel(server, NewName(name)) for _, flag := range flags { channel.flags[ChannelMode(flag)] = true } - channel.key = key - channel.topic = topic + channel.key = NewText(key) + channel.topic = NewText(topic) channel.userLimit = userLimit loadChannelList(channel, banList, BanMask) loadChannelList(channel, exceptList, ExceptMask) @@ -107,38 +119,35 @@ func (server *Server) loadChannels() { func (server *Server) processCommand(cmd Command) { client := cmd.Client() - if DEBUG_SERVER { - log.Printf("%s → %s %s", client, server, cmd) - } + Log.debug.Printf("%s → %s %s", client, server, cmd) - switch client.phase { - case Registration: + if !client.registered { regCmd, ok := cmd.(RegServerCommand) if !ok { client.Quit("unexpected command") return } regCmd.HandleRegServer(server) - - case Normal: - srvCmd, ok := cmd.(ServerCommand) - if !ok { - client.ErrUnknownCommand(cmd.Code()) - return - } - switch srvCmd.(type) { - case *PingCommand, *PongCommand: - client.Touch() - - case *QuitCommand: - // no-op - - default: - client.Active() - client.Touch() - } - srvCmd.HandleServer(server) + return } + + srvCmd, ok := cmd.(ServerCommand) + if !ok { + client.ErrUnknownCommand(cmd.Code()) + return + } + switch srvCmd.(type) { + case *PingCommand, *PongCommand: + client.Touch() + + case *QuitCommand: + // no-op + + default: + client.Active() + client.Touch() + } + srvCmd.HandleServer(server) } func (server *Server) Shutdown() { @@ -178,21 +187,15 @@ func (s *Server) listen(addr string) { log.Fatal(s, "listen error: ", err) } - if DEBUG_SERVER { - log.Printf("%s listening on %s", s, addr) - } + Log.info.Printf("%s listening on %s", s, addr) for { conn, err := listener.Accept() if err != nil { - if DEBUG_SERVER { - log.Printf("%s accept error: %s", s, err) - } + Log.error.Printf("%s accept error: %s", s, err) continue } - if DEBUG_SERVER { - log.Printf("%s accept: %s", s, conn.RemoteAddr()) - } + Log.debug.Printf("%s accept: %s", s, conn.RemoteAddr()) s.newConns <- conn } @@ -203,14 +206,17 @@ func (s *Server) listen(addr string) { // func (s *Server) tryRegister(c *Client) { - if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) { - c.Register() - c.RplWelcome() - c.RplYourHost() - c.RplCreated() - c.RplMyInfo() - s.MOTD(c) + if c.registered || !c.HasNick() || !c.HasUsername() || + (c.capState == CapNegotiating) { + return } + + c.Register() + c.RplWelcome() + c.RplYourHost() + c.RplCreated() + c.RplMyInfo() + s.MOTD(c) } func (server *Server) MOTD(client *Client) { @@ -281,44 +287,6 @@ func (msg *ProxyCommand) HandleRegServer(server *Server) { msg.Client().hostname = msg.hostname } -func (msg *CapCommand) HandleRegServer(server *Server) { - client := msg.Client() - - switch msg.subCommand { - case CAP_LS: - client.capState = CapNegotiating - client.Reply(RplCap(client, CAP_LS, SupportedCapabilities)) - - case CAP_LIST: - client.Reply(RplCap(client, CAP_LIST, client.capabilities)) - - case CAP_REQ: - client.capState = CapNegotiating - for capability := range msg.capabilities { - if !SupportedCapabilities[capability] { - client.Reply(RplCap(client, CAP_NAK, msg.capabilities)) - return - } - } - for capability := range msg.capabilities { - client.capabilities[capability] = true - } - client.Reply(RplCap(client, CAP_ACK, msg.capabilities)) - - case CAP_CLEAR: - reply := RplCap(client, CAP_ACK, client.capabilities.DisableString()) - client.capabilities = make(CapabilitySet) - client.Reply(reply) - - case CAP_END: - client.capState = CapNegotiated - server.tryRegister(client) - - default: - client.ErrInvalidCapCmd(msg.subCommand) - } -} - func (m *NickCommand) HandleRegServer(s *Server) { client := m.Client() if !client.authorized { @@ -369,7 +337,7 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { } flags := msg.Flags() if len(flags) > 0 { - for _, mode := range msg.Flags() { + for _, mode := range flags { client.flags[mode] = true } client.RplUModeIs(client) @@ -521,58 +489,6 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) { } } -func (m *ModeCommand) HandleServer(s *Server) { - client := m.Client() - target := s.clients.Get(m.nickname) - - if target == nil { - client.ErrNoSuchNick(m.nickname) - return - } - - if client != target && !client.flags[Operator] { - client.ErrUsersDontMatch() - return - } - - changes := make(ModeChanges, 0, len(m.changes)) - - for _, change := range m.changes { - switch change.mode { - case Invisible, ServerNotice, WallOps: - switch change.op { - case Add: - if target.flags[change.mode] { - continue - } - target.flags[change.mode] = true - changes = append(changes, change) - - case Remove: - if !target.flags[change.mode] { - continue - } - delete(target.flags, change.mode) - changes = append(changes, change) - } - - case Operator, LocalOperator: - if change.op == Remove { - if !target.flags[change.mode] { - continue - } - delete(target.flags, change.mode) - changes = append(changes, change) - } - } - } - - // Who should get these replies? - if len(changes) > 0 { - client.Reply(RplMode(client, target, changes)) - } -} - func (client *Client) WhoisChannelsNames() []string { chstrs := make([]string, len(client.channels)) index := 0 @@ -609,17 +525,6 @@ func (m *WhoisCommand) HandleServer(server *Server) { } } -func (msg *ChannelModeCommand) HandleServer(server *Server) { - client := msg.Client() - channel := server.channels.Get(msg.channel) - if channel == nil { - client.ErrNoSuchChannel(msg.channel) - return - } - - channel.Mode(client, msg.changes) -} - func whoChannel(client *Client, channel *Channel, friends ClientSet) { for member := range channel.members { if !client.flags[Invisible] || friends[client] { diff --git a/irc/socket.go b/irc/socket.go index 48a663e9..84e90313 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -3,7 +3,6 @@ package irc import ( "bufio" "io" - "log" "net" "strings" ) @@ -20,7 +19,7 @@ type Socket struct { writer *bufio.Writer } -func NewSocket(conn net.Conn, commands chan<- editableCommand) *Socket { +func NewSocket(conn net.Conn, commands chan<- Command) *Socket { socket := &Socket{ conn: conn, reader: bufio.NewReader(conn), @@ -38,12 +37,10 @@ func (socket *Socket) String() string { func (socket *Socket) Close() { socket.conn.Close() - if DEBUG_NET { - log.Printf("%s closed", socket) - } + Log.debug.Printf("%s closed", socket) } -func (socket *Socket) readLines(commands chan<- editableCommand) { +func (socket *Socket) readLines(commands chan<- Command) { commands <- &ProxyCommand{ hostname: AddrLookupHostname(socket.conn.RemoteAddr()), } @@ -57,9 +54,7 @@ func (socket *Socket) readLines(commands chan<- editableCommand) { if len(line) == 0 { continue } - if DEBUG_NET { - log.Printf("%s → %s", socket, line) - } + Log.debug.Printf("%s → %s", socket, line) msg, err := ParseCommand(line) if err != nil { @@ -87,16 +82,14 @@ func (socket *Socket) Write(line string) (err error) { return } - if DEBUG_NET { - log.Printf("%s ← %s", socket, line) - } + Log.debug.Printf("%s ← %s", socket, line) return } func (socket *Socket) isError(err error, dir rune) bool { if err != nil { - if DEBUG_NET && (err != io.EOF) { - log.Printf("%s %c error: %s", socket, dir, err) + if err != io.EOF { + Log.debug.Printf("%s %c error: %s", socket, dir, err) } return true } diff --git a/irc/types.go b/irc/types.go index ecf53d42..7c8a5a2f 100644 --- a/irc/types.go +++ b/irc/types.go @@ -9,83 +9,6 @@ import ( // simple types // -type CapSubCommand string - -type Capability string - -func (capability Capability) String() string { - return string(capability) -} - -type CapModifier rune - -func (mod CapModifier) String() string { - return string(mod) -} - -type CapState uint - -type CapabilitySet map[Capability]bool - -func (set CapabilitySet) String() string { - strs := make([]string, len(set)) - index := 0 - for capability := range set { - strs[index] = string(capability) - index += 1 - } - return strings.Join(strs, " ") -} - -func (set CapabilitySet) DisableString() string { - parts := make([]string, len(set)) - index := 0 - for capability := range set { - parts[index] = Disable.String() + capability.String() - index += 1 - } - return strings.Join(parts, " ") -} - -// add, remove, list modes -type ModeOp rune - -func (op ModeOp) String() string { - return string(op) -} - -// user mode flags -type UserMode rune - -func (mode UserMode) String() string { - return string(mode) -} - -type Phase uint - -type ReplyCode interface { - String() string -} - -type StringCode Name - -func (code StringCode) String() string { - return string(code) -} - -type NumericCode uint - -func (code NumericCode) String() string { - return fmt.Sprintf("%03d", code) -} - -// channel mode flags -type ChannelMode rune - -func (mode ChannelMode) String() string { - return string(mode) -} - type ChannelNameMap map[Name]*Channel func (channels ChannelNameMap) Get(name Name) *Channel { @@ -181,31 +104,7 @@ func (channels ChannelSet) First() *Channel { // interfaces // -type Identifier interface { +type Identifiable interface { Id() Name Nick() Name } - -type Replier interface { - Reply(...string) -} - -type Command interface { - Code() StringCode - Client() *Client -} - -type ServerCommand interface { - Command - HandleServer(*Server) -} - -type AuthServerCommand interface { - Command - HandleAuthServer(*Server) -} - -type RegServerCommand interface { - Command - HandleRegServer(*Server) -}