From 72726a39b82fe70ffc538051f8c3c6b3bd45a893 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Mon, 24 Feb 2014 09:41:09 -0800 Subject: [PATCH] many changes - load config sub files relative to config file dir - load config file by name - expect bcrypt for passwords - -genpasswd for generating config-file-safe passwords - block client thread while checking passwords (PASS and OPER) --- README.md | 23 ++++++++++++----- irc/client.go | 69 ++++++++++--------------------------------------- irc/commands.go | 43 +++++++++++++++++++++++------- irc/config.go | 8 ++++++ irc/server.go | 41 +++++------------------------ 5 files changed, 77 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 5f8a59e2..9bc383b8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,17 @@ Ergonomadic is an IRC daemon written from scratch in Go. +## Some Features + +- follows the RFC where possible +- JSON-based configuration +- server password +- channels with many standard modes +- IRC operators +- TLS support (but better to use stunnel with proxy protocol) +- haproxy PROXY protocol header for hostname setting +- passwords stored in bcrypt format + ## Why? I wanted to learn Go. @@ -20,16 +31,14 @@ I wanted to learn Go. ## Running the Server -You must create an `ergonomadic.json` config file in the current directory. +See the example `config.json`. Passwords are base64-encoded bcrypted +byte strings. You can generate them with e.g. `ergonomadic -genpasswd +'hunter21!'`. ### from your GOPATH ```sh +go get go install -ergonomadic -``` - -### from local -```sh -go run ergonomadic.go +ergonomadic -conf '/path/to/ergonomadic.json' ``` diff --git a/irc/client.go b/irc/client.go index 7ce5ceab..7047f164 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1,7 +1,6 @@ package irc import ( - "code.google.com/p/go.crypto/bcrypt" "fmt" "log" "net" @@ -16,7 +15,6 @@ type Client struct { atime time.Time awayMessage string channels ChannelSet - checkPass chan CheckCommand commands chan editableCommand ctime time.Time flags map[UserMode]bool @@ -37,14 +35,13 @@ type Client struct { func NewClient(server *Server, conn net.Conn) *Client { now := time.Now() client := &Client{ - atime: now, - channels: make(ChannelSet), - checkPass: make(chan CheckCommand), - commands: make(chan editableCommand), - ctime: now, - flags: make(map[UserMode]bool), - phase: server.InitPhase(), - server: server, + atime: now, + channels: make(ChannelSet), + commands: make(chan editableCommand), + ctime: now, + flags: make(map[UserMode]bool), + phase: server.InitPhase(), + server: server, } client.socket = NewSocket(conn, client.commands) client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout) @@ -57,56 +54,17 @@ func NewClient(server *Server, conn net.Conn) *Client { // command goroutine // -type CheckCommand interface { - Response() editableCommand -} - -type CheckedPassCommand struct { - BaseCommand - isRight bool -} - -type CheckPassCommand struct { - hash []byte - password []byte -} - -func (cmd *CheckPassCommand) CheckPassword() bool { - return bcrypt.CompareHashAndPassword(cmd.hash, cmd.password) == nil -} - -func (cmd *CheckPassCommand) Response() editableCommand { - return &CheckedPassCommand{ - isRight: cmd.CheckPassword(), - } -} - -type CheckOperCommand struct { - CheckPassCommand -} - -type CheckedOperCommand struct { - CheckedPassCommand -} - -func (cmd *CheckOperCommand) Response() editableCommand { - response := &CheckedOperCommand{} - response.isRight = cmd.CheckPassword() - return response -} - func (client *Client) run() { for command := range client.commands { command.SetClient(client) - client.server.commands <- command - switch command.(type) { - case *PassCommand, *OperCommand: - cmd := <-client.checkPass - response := cmd.Response() - response.SetClient(client) - client.server.commands <- response + checkPass, ok := command.(checkPasswordCommand) + if ok { + checkPass.LoadPassword(client.server) + checkPass.CheckPassword() } + + client.server.commands <- command } } @@ -159,7 +117,6 @@ func (client *Client) Idle() { func (client *Client) Register() { client.phase = Normal client.loginTimer.Stop() - client.loginTimer = nil client.Touch() } diff --git a/irc/commands.go b/irc/commands.go index b57a0753..b228fe93 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -1,6 +1,7 @@ package irc import ( + "code.google.com/p/go.crypto/bcrypt" "errors" "fmt" "regexp" @@ -14,6 +15,11 @@ type editableCommand interface { SetClient(*Client) } +type checkPasswordCommand interface { + LoadPassword(*Server) + CheckPassword() +} + type parseCommandFunc func([]string) (editableCommand, error) var ( @@ -188,19 +194,34 @@ func NewPongCommand(args []string) (editableCommand, error) { type PassCommand struct { BaseCommand - password string + hash []byte + password []byte + err error } +var ErrCannotCheck = errors.New("cannot check password") + func (cmd *PassCommand) String() string { return fmt.Sprintf("PASS(password=%s)", cmd.password) } +func (cmd *PassCommand) LoadPassword(server *Server) { + cmd.hash = server.password +} + +func (cmd *PassCommand) CheckPassword() { + if cmd.hash == nil { + return + } + cmd.err = bcrypt.CompareHashAndPassword(cmd.hash, cmd.password) +} + func NewPassCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } return &PassCommand{ - password: args[0], + password: []byte(args[0]), }, nil } @@ -652,25 +673,29 @@ func (msg *WhoCommand) String() string { } type OperCommand struct { - BaseCommand - name string - password string + PassCommand + name string } func (msg *OperCommand) String() string { return fmt.Sprintf("OPER(name=%s, password=%s)", msg.name, msg.password) } +func (msg *OperCommand) LoadPassword(server *Server) { + msg.hash = server.operators[msg.name] +} + // OPER func NewOperCommand(args []string) (editableCommand, error) { if len(args) < 2 { return nil, NotEnoughArgsError } - return &OperCommand{ - name: args[0], - password: args[1], - }, nil + cmd := &OperCommand{ + name: args[0], + } + cmd.password = []byte(args[1]) + return cmd, nil } // TODO diff --git a/irc/config.go b/irc/config.go index 7f07d69b..5ddebb21 100644 --- a/irc/config.go +++ b/irc/config.go @@ -32,6 +32,14 @@ func (conf *Config) PasswordBytes() []byte { return decodePassword(conf.Password) } +func (conf *Config) OperatorsMap() map[string][]byte { + operators := make(map[string][]byte) + for _, opConf := range conf.Operators { + operators[opConf.Name] = opConf.PasswordBytes() + } + return operators +} + type OperatorConfig struct { Name string Password string diff --git a/irc/server.go b/irc/server.go index 4e518ba8..0ff45e01 100644 --- a/irc/server.go +++ b/irc/server.go @@ -34,21 +34,17 @@ func NewServer(config *Config) *Server { server := &Server{ channels: make(ChannelNameMap), clients: make(ClientNameMap), - commands: make(chan Command), + commands: make(chan Command, 16), ctime: time.Now(), - idle: make(chan *Client), + idle: make(chan *Client, 16), motdFile: config.MOTD, name: config.Name, - newConns: make(chan net.Conn), - operators: make(map[string][]byte), + newConns: make(chan net.Conn, 16), + operators: config.OperatorsMap(), password: config.PasswordBytes(), timeout: make(chan *Client), } - for _, opConf := range config.Operators { - server.operators[opConf.Name] = opConf.PasswordBytes() - } - for _, listenerConf := range config.Listeners { go server.listen(listenerConf) } @@ -266,18 +262,7 @@ func (msg *CapCommand) HandleAuthServer(server *Server) { func (msg *PassCommand) HandleAuthServer(server *Server) { client := msg.Client() - cmd := &CheckPassCommand{ - hash: server.password, - password: []byte(msg.password), - } - go func() { - client.checkPass <- cmd - }() -} - -func (msg *CheckedPassCommand) HandleAuthServer(server *Server) { - client := msg.Client() - if !msg.isRight { + if msg.err != nil { client.ErrPasswdMismatch() client.Quit("bad password") return @@ -603,21 +588,7 @@ func (msg *WhoCommand) HandleServer(server *Server) { func (msg *OperCommand) HandleServer(server *Server) { client := msg.Client() - cmd := &CheckOperCommand{} - cmd.hash = server.operators[msg.name] - if cmd.hash == nil { - client.ErrPasswdMismatch() - return - } - cmd.password = []byte(msg.password) - go func() { - client.checkPass <- cmd - }() -} - -func (msg *CheckedOperCommand) HandleServer(server *Server) { - client := msg.Client() - if !msg.isRight { + if (msg.hash == nil) || (msg.err != nil) { client.ErrPasswdMismatch() return }