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

@ -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'
```

@ -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()
}

@ -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 <name> <password>
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

@ -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

@ -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
}