diff --git a/config.json b/config.json new file mode 100644 index 00000000..d33117ab --- /dev/null +++ b/config.json @@ -0,0 +1,13 @@ +{ "name": "irc.example.com", + "motd": "motd.txt", + "listeners": [ + { "address": "localhost:7777" }, + { "address": "[::1]:7777" } ], + "operators": [ + { "name": "root", + "password": "JDJhJDEwJFRWWGUya2E3Unk5bnZlb2o3alJ0ZnVQQm9ZVW1HOE53L29nVHg5QWh5TnpaMmtOaEwya1Vl" } ], + "debug": { + "net": true, + "client": false, + "channel": false, + "server": false } } diff --git a/ergonomadic.go b/ergonomadic.go index 94a35def..47d3a850 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -1,12 +1,35 @@ package main import ( + "code.google.com/p/go.crypto/bcrypt" + "encoding/base64" + "flag" + "fmt" "github.com/jlatt/ergonomadic/irc" "log" ) +func genPasswd(passwd string) { + log.Printf("encoding password \"%s\"\n", passwd) + crypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost) + if err != nil { + log.Fatal(err) + } + encoded := base64.StdEncoding.EncodeToString(crypted) + fmt.Println(encoded) +} + func main() { - config, err := irc.LoadConfig() + conf := flag.String("conf", "ergonomadic.json", "ergonomadic config file") + passwd := flag.String("genpasswd", "", "bcrypt a password") + flag.Parse() + + if *passwd != "" { + genPasswd(*passwd) + return + } + + config, err := irc.LoadConfig(*conf) if err != nil { log.Fatal(err) return diff --git a/irc/client.go b/irc/client.go index 2bfa7de1..7ce5ceab 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1,6 +1,7 @@ package irc import ( + "code.google.com/p/go.crypto/bcrypt" "fmt" "log" "net" @@ -15,6 +16,8 @@ type Client struct { atime time.Time awayMessage string channels ChannelSet + checkPass chan CheckCommand + commands chan editableCommand ctime time.Time flags map[UserMode]bool hasQuit bool @@ -34,19 +37,79 @@ type Client struct { func NewClient(server *Server, conn net.Conn) *Client { now := time.Now() client := &Client{ - atime: now, - channels: make(ChannelSet), - ctime: now, - flags: make(map[UserMode]bool), - phase: server.InitPhase(), - server: server, + 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, } - client.socket = NewSocket(conn, client, server.commands) + client.socket = NewSocket(conn, client.commands) client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout) + go client.run() return 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 + } + } +} + // // idle timer goroutine // @@ -106,7 +169,6 @@ func (client *Client) destroy() { for channel := range client.channels { channel.Quit(client) } - client.channels = nil // clean up server @@ -116,22 +178,16 @@ func (client *Client) destroy() { if client.loginTimer != nil { client.loginTimer.Stop() - client.loginTimer = nil } if client.idleTimer != nil { client.idleTimer.Stop() - client.idleTimer = nil } if client.quitTimer != nil { client.quitTimer.Stop() - client.quitTimer = nil } client.socket.Close() - client.socket = nil - client.server = nil - if DEBUG_CLIENT { log.Printf("%s: destroyed", client) } diff --git a/irc/config.go b/irc/config.go index 3e394a04..7f07d69b 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1,10 +1,24 @@ package irc import ( + "encoding/base64" "encoding/json" + "log" "os" + "path/filepath" ) +func decodePassword(password string) []byte { + if password == "" { + return nil + } + bytes, err := base64.StdEncoding.DecodeString(password) + if err != nil { + log.Fatal(err) + } + return bytes +} + type Config struct { Debug map[string]bool Listeners []ListenerConfig @@ -14,11 +28,19 @@ type Config struct { Password string } +func (conf *Config) PasswordBytes() []byte { + return decodePassword(conf.Password) +} + type OperatorConfig struct { Name string Password string } +func (conf *OperatorConfig) PasswordBytes() []byte { + return decodePassword(conf.Password) +} + type ListenerConfig struct { Net string Address string @@ -30,10 +52,10 @@ func (config *ListenerConfig) IsTLS() bool { return (config.Key != "") && (config.Certificate != "") } -func LoadConfig() (config *Config, err error) { +func LoadConfig(filename string) (config *Config, err error) { config = &Config{} - file, err := os.Open("ergonomadic.json") + file, err := os.Open(filename) if err != nil { return } @@ -44,6 +66,10 @@ func LoadConfig() (config *Config, err error) { if err != nil { return } + + dir := filepath.Dir(filename) + config.MOTD = filepath.Join(dir, config.MOTD) + for _, lconf := range config.Listeners { if lconf.Net == "" { lconf.Net = "tcp" diff --git a/irc/server.go b/irc/server.go index 957fe8d9..4e518ba8 100644 --- a/irc/server.go +++ b/irc/server.go @@ -25,8 +25,8 @@ type Server struct { motdFile string name string newConns chan net.Conn - operators map[string]string - password string + operators map[string][]byte + password []byte timeout chan *Client } @@ -34,19 +34,19 @@ func NewServer(config *Config) *Server { server := &Server{ channels: make(ChannelNameMap), clients: make(ClientNameMap), - commands: make(chan Command, 16), + commands: make(chan Command), ctime: time.Now(), - idle: make(chan *Client, 16), + idle: make(chan *Client), motdFile: config.MOTD, name: config.Name, - newConns: make(chan net.Conn, 16), - operators: make(map[string]string), - password: config.Password, + newConns: make(chan net.Conn), + operators: make(map[string][]byte), + password: config.PasswordBytes(), timeout: make(chan *Client), } for _, opConf := range config.Operators { - server.operators[opConf.Name] = opConf.Password + server.operators[opConf.Name] = opConf.PasswordBytes() } for _, listenerConf := range config.Listeners { @@ -106,20 +106,20 @@ func (server *Server) Run() { case conn := <-server.newConns: NewClient(server, conn) + case cmd := <-server.commands: + server.ProcessCommand(cmd) + case client := <-server.idle: client.Idle() case client := <-server.timeout: client.Quit("connection timeout") - - case cmd := <-server.commands: - server.ProcessCommand(cmd) } } } func (server *Server) InitPhase() Phase { - if server.password == "" { + if server.password == nil { return Registration } return Authorization @@ -264,10 +264,20 @@ func (msg *CapCommand) HandleAuthServer(server *Server) { // TODO } -func (m *PassCommand) HandleAuthServer(s *Server) { - client := m.Client() +func (msg *PassCommand) HandleAuthServer(server *Server) { + client := msg.Client() + cmd := &CheckPassCommand{ + hash: server.password, + password: []byte(msg.password), + } + go func() { + client.checkPass <- cmd + }() +} - if s.password != m.password { +func (msg *CheckedPassCommand) HandleAuthServer(server *Server) { + client := msg.Client() + if !msg.isRight { client.ErrPasswdMismatch() client.Quit("bad password") return @@ -593,13 +603,26 @@ func (msg *WhoCommand) HandleServer(server *Server) { func (msg *OperCommand) HandleServer(server *Server) { client := msg.Client() - if server.operators[msg.name] != msg.password { + 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 { client.ErrPasswdMismatch() return } client.flags[Operator] = true - client.RplYoureOper() client.RplUModeIs(client) } diff --git a/irc/socket.go b/irc/socket.go index 3aba4e30..a93e7a5b 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -15,17 +15,15 @@ const ( ) type Socket struct { - client *Client conn net.Conn reader *bufio.Reader writer *bufio.Writer } -func NewSocket(conn net.Conn, client *Client, commands chan<- Command) *Socket { +func NewSocket(conn net.Conn, commands chan<- editableCommand) *Socket { socket := &Socket{ conn: conn, reader: bufio.NewReader(conn), - client: client, writer: bufio.NewWriter(conn), } @@ -45,12 +43,10 @@ func (socket *Socket) Close() { } } -func (socket *Socket) readLines(commands chan<- Command) { - hostnameLookup := &ProxyCommand{ +func (socket *Socket) readLines(commands chan<- editableCommand) { + commands <- &ProxyCommand{ hostname: AddrLookupHostname(socket.conn.RemoteAddr()), } - hostnameLookup.SetClient(socket.client) - commands <- hostnameLookup for { line, err := socket.reader.ReadString('\n') @@ -70,15 +66,12 @@ func (socket *Socket) readLines(commands chan<- Command) { // TODO error messaging to client continue } - msg.SetClient(socket.client) commands <- msg } - msg := &QuitCommand{ + commands <- &QuitCommand{ message: "connection closed", } - msg.SetClient(socket.client) - commands <- msg } func (socket *Socket) Write(line string) (err error) {