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)
This commit is contained in:
Jeremy Latt 2014-02-24 09:41:09 -08:00
parent be089e7f5f
commit 72726a39b8
5 changed files with 77 additions and 107 deletions

View File

@ -2,6 +2,17 @@
Ergonomadic is an IRC daemon written from scratch in Go. 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? ## Why?
I wanted to learn Go. I wanted to learn Go.
@ -20,16 +31,14 @@ I wanted to learn Go.
## Running the Server ## 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 ### from your GOPATH
```sh ```sh
go get
go install go install
ergonomadic ergonomadic -conf '/path/to/ergonomadic.json'
```
### from local
```sh
go run ergonomadic.go
``` ```

View File

@ -1,7 +1,6 @@
package irc package irc
import ( import (
"code.google.com/p/go.crypto/bcrypt"
"fmt" "fmt"
"log" "log"
"net" "net"
@ -16,7 +15,6 @@ type Client struct {
atime time.Time atime time.Time
awayMessage string awayMessage string
channels ChannelSet channels ChannelSet
checkPass chan CheckCommand
commands chan editableCommand commands chan editableCommand
ctime time.Time ctime time.Time
flags map[UserMode]bool flags map[UserMode]bool
@ -37,14 +35,13 @@ type Client struct {
func NewClient(server *Server, conn net.Conn) *Client { func NewClient(server *Server, conn net.Conn) *Client {
now := time.Now() now := time.Now()
client := &Client{ client := &Client{
atime: now, atime: now,
channels: make(ChannelSet), channels: make(ChannelSet),
checkPass: make(chan CheckCommand), commands: make(chan editableCommand),
commands: make(chan editableCommand), ctime: now,
ctime: now, flags: make(map[UserMode]bool),
flags: make(map[UserMode]bool), phase: server.InitPhase(),
phase: server.InitPhase(), server: server,
server: server,
} }
client.socket = NewSocket(conn, client.commands) client.socket = NewSocket(conn, client.commands)
client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout) client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout)
@ -57,56 +54,17 @@ func NewClient(server *Server, conn net.Conn) *Client {
// command goroutine // 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() { func (client *Client) run() {
for command := range client.commands { for command := range client.commands {
command.SetClient(client) command.SetClient(client)
client.server.commands <- command
switch command.(type) { checkPass, ok := command.(checkPasswordCommand)
case *PassCommand, *OperCommand: if ok {
cmd := <-client.checkPass checkPass.LoadPassword(client.server)
response := cmd.Response() checkPass.CheckPassword()
response.SetClient(client)
client.server.commands <- response
} }
client.server.commands <- command
} }
} }
@ -159,7 +117,6 @@ func (client *Client) Idle() {
func (client *Client) Register() { func (client *Client) Register() {
client.phase = Normal client.phase = Normal
client.loginTimer.Stop() client.loginTimer.Stop()
client.loginTimer = nil
client.Touch() client.Touch()
} }

View File

@ -1,6 +1,7 @@
package irc package irc
import ( import (
"code.google.com/p/go.crypto/bcrypt"
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
@ -14,6 +15,11 @@ type editableCommand interface {
SetClient(*Client) SetClient(*Client)
} }
type checkPasswordCommand interface {
LoadPassword(*Server)
CheckPassword()
}
type parseCommandFunc func([]string) (editableCommand, error) type parseCommandFunc func([]string) (editableCommand, error)
var ( var (
@ -188,19 +194,34 @@ func NewPongCommand(args []string) (editableCommand, error) {
type PassCommand struct { type PassCommand struct {
BaseCommand BaseCommand
password string hash []byte
password []byte
err error
} }
var ErrCannotCheck = errors.New("cannot check password")
func (cmd *PassCommand) String() string { func (cmd *PassCommand) String() string {
return fmt.Sprintf("PASS(password=%s)", cmd.password) 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) { func NewPassCommand(args []string) (editableCommand, error) {
if len(args) < 1 { if len(args) < 1 {
return nil, NotEnoughArgsError return nil, NotEnoughArgsError
} }
return &PassCommand{ return &PassCommand{
password: args[0], password: []byte(args[0]),
}, nil }, nil
} }
@ -652,25 +673,29 @@ func (msg *WhoCommand) String() string {
} }
type OperCommand struct { type OperCommand struct {
BaseCommand PassCommand
name string name string
password string
} }
func (msg *OperCommand) String() string { func (msg *OperCommand) String() string {
return fmt.Sprintf("OPER(name=%s, password=%s)", msg.name, msg.password) 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 <name> <password> // OPER <name> <password>
func NewOperCommand(args []string) (editableCommand, error) { func NewOperCommand(args []string) (editableCommand, error) {
if len(args) < 2 { if len(args) < 2 {
return nil, NotEnoughArgsError return nil, NotEnoughArgsError
} }
return &OperCommand{ cmd := &OperCommand{
name: args[0], name: args[0],
password: args[1], }
}, nil cmd.password = []byte(args[1])
return cmd, nil
} }
// TODO // TODO

View File

@ -32,6 +32,14 @@ func (conf *Config) PasswordBytes() []byte {
return decodePassword(conf.Password) 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 { type OperatorConfig struct {
Name string Name string
Password string Password string

View File

@ -34,21 +34,17 @@ func NewServer(config *Config) *Server {
server := &Server{ server := &Server{
channels: make(ChannelNameMap), channels: make(ChannelNameMap),
clients: make(ClientNameMap), clients: make(ClientNameMap),
commands: make(chan Command), commands: make(chan Command, 16),
ctime: time.Now(), ctime: time.Now(),
idle: make(chan *Client), idle: make(chan *Client, 16),
motdFile: config.MOTD, motdFile: config.MOTD,
name: config.Name, name: config.Name,
newConns: make(chan net.Conn), newConns: make(chan net.Conn, 16),
operators: make(map[string][]byte), operators: config.OperatorsMap(),
password: config.PasswordBytes(), password: config.PasswordBytes(),
timeout: make(chan *Client), timeout: make(chan *Client),
} }
for _, opConf := range config.Operators {
server.operators[opConf.Name] = opConf.PasswordBytes()
}
for _, listenerConf := range config.Listeners { for _, listenerConf := range config.Listeners {
go server.listen(listenerConf) go server.listen(listenerConf)
} }
@ -266,18 +262,7 @@ func (msg *CapCommand) HandleAuthServer(server *Server) {
func (msg *PassCommand) HandleAuthServer(server *Server) { func (msg *PassCommand) HandleAuthServer(server *Server) {
client := msg.Client() client := msg.Client()
cmd := &CheckPassCommand{ if msg.err != nil {
hash: server.password,
password: []byte(msg.password),
}
go func() {
client.checkPass <- cmd
}()
}
func (msg *CheckedPassCommand) HandleAuthServer(server *Server) {
client := msg.Client()
if !msg.isRight {
client.ErrPasswdMismatch() client.ErrPasswdMismatch()
client.Quit("bad password") client.Quit("bad password")
return return
@ -603,21 +588,7 @@ func (msg *WhoCommand) HandleServer(server *Server) {
func (msg *OperCommand) HandleServer(server *Server) { func (msg *OperCommand) HandleServer(server *Server) {
client := msg.Client() client := msg.Client()
cmd := &CheckOperCommand{} if (msg.hash == nil) || (msg.err != nil) {
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 {
client.ErrPasswdMismatch() client.ErrPasswdMismatch()
return return
} }