From a939b027fdb6ce1d0822e76ec50814678ff0050a Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 14 Aug 2014 14:01:54 +0400 Subject: [PATCH] =?UTF-8?q?Ability=20to=20authenticate=20users=20by=20nick?= =?UTF-8?q?name=E2=86=94password=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README | 17 ++++++++++++++++- client.go | 4 ++-- daemon.go | 40 ++++++++++++++++++++++++++++++++++++++++ goircd.go | 26 +++++++++++++++++++++----- room.go | 4 ++++ 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/README b/README index 03f31d8..852e455 100644 --- a/README +++ b/README @@ -28,6 +28,7 @@ But it has some convincing features: * Optional permanent channel's state saving in plain text files (so you can reload daemon and all channels topics and keys won't disappear) +* Optional ability to authenticate users by nickname↔password Some remarks and recommendations related to it's simplicity: @@ -36,7 +37,7 @@ Some remarks and recommendations related to it's simplicity: SUPPORTED IRC COMMANDS -* NICK/USER during registration workflow +* PASS/NICK/USER during registration workflow * PING/PONGs * NOTICE/PRIVMSG * MOTD, LUSERS, WHO, WHOIS, QUIT @@ -56,8 +57,22 @@ Just execute goircd daemon. It has following optional arguments: loaded during startup. If omitted, then states will be lost after daemon termination * -tls_key/-tls_cert: enable TLS and specify key and certificate file +* -passwords: enable client authentication and specify path to + passwords file * -verbose: increase log messages verbosity +AUTHENTICATION + +You can turn on optional client authentication by preparing passwords +file and using the -passwords argument. Format of passwords file is: + + login1:password1\n + login2:password2\n + … + +You can force rereading of passwords file without server interruption by +sending HUP signal to him. + LICENCE This program is free software: you can redistribute it and/or modify diff --git a/client.go b/client.go index afd6312..4f48daf 100644 --- a/client.go +++ b/client.go @@ -37,6 +37,7 @@ type Client struct { nickname string username string realname string + password string } type ClientAlivenessState struct { @@ -49,7 +50,7 @@ func (client Client) String() string { } func NewClient(hostname string, conn net.Conn) *Client { - return &Client{hostname: hostname, conn: conn, nickname: "*"} + return &Client{hostname: hostname, conn: conn, nickname: "*", password: ""} } // Client processor blockingly reads everything remote client sends, @@ -64,7 +65,6 @@ func (client *Client) Processor(sink chan<- ClientEvent) { bufNet = make([]byte, BufSize) _, err := client.conn.Read(bufNet) if err != nil { - log.Println(client, "connection lost", err) sink <- ClientEvent{client, EventDel, ""} break } diff --git a/daemon.go b/daemon.go index 06916b0..ffd2ef1 100644 --- a/daemon.go +++ b/daemon.go @@ -25,6 +25,7 @@ import ( "regexp" "sort" "strings" + "sync" "time" ) @@ -38,6 +39,8 @@ var ( RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,9}$") ) +var passwordsRefreshLock sync.Mutex + type Daemon struct { Verbose bool hostname string @@ -49,6 +52,7 @@ type Daemon struct { lastAlivenessCheck time.Time logSink chan<- LogEvent stateSink chan<- StateEvent + passwords map[string]string } func NewDaemon(hostname, motd string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Daemon { @@ -154,6 +158,12 @@ func (daemon *Daemon) SendList(client *Client, cols []string) { // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him. func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) { switch command { + case "PASS": + if len(cols) == 1 || len(cols[1]) < 1 { + client.ReplyNotEnoughParameters("PASS") + return + } + client.password = cols[1] case "NICK": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyParts("431", "No nickname given") @@ -185,6 +195,14 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri client.realname = strings.TrimLeft(args[3], ":") } if client.nickname != "*" && client.username != "" { + passwordsRefreshLock.Lock() + if daemon.passwords != nil && (client.password == "" || daemon.passwords[client.nickname] != client.password) { + passwordsRefreshLock.Unlock() + client.ReplyParts("462", "You may not register") + client.conn.Close() + return + } + passwordsRefreshLock.Unlock() client.registered = true client.ReplyNicknamed("001", "Hi, welcome to IRC") client.ReplyNicknamed("002", "Your host is "+daemon.hostname+", running goircd") @@ -192,6 +210,7 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri client.ReplyNicknamed("004", daemon.hostname+" goircd o o") daemon.SendLusers(client) daemon.SendMotd(client) + log.Println(client, "logged in") } } @@ -247,6 +266,7 @@ func (daemon *Daemon) HandlerJoin(client *Client, cmd string) { continue } roomNew, roomSink := daemon.RoomRegister(room) + log.Println("Room", roomNew, "created") if key != "" { roomNew.key = key roomNew.StateSave() @@ -302,6 +322,7 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { log.Println(client, "command", command) } if command == "QUIT" { + log.Println(client, "quit") delete(daemon.clients, client) delete(daemon.clientAliveness, client) client.conn.Close() @@ -447,3 +468,22 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { } } } + +func (daemon *Daemon) PasswordsRefresh() { + contents, err := ioutil.ReadFile(*passwords) + if err != nil { + log.Fatalf("Can no read passwords file %s: %s", *passwords, err) + return + } + processed := make(map[string]string) + for _, entry := range strings.Split(string(contents), "\n") { + loginAndPassword := strings.Split(entry, ":") + if len(loginAndPassword) == 2 { + processed[loginAndPassword[0]] = loginAndPassword[1] + } + } + log.Printf("Read %d passwords", len(processed)) + passwordsRefreshLock.Lock() + daemon.passwords = processed + passwordsRefreshLock.Unlock() +} diff --git a/goircd.go b/goircd.go index dbf6d7d..984b47b 100644 --- a/goircd.go +++ b/goircd.go @@ -23,17 +23,21 @@ import ( "io/ioutil" "log" "net" + "os" + "os/signal" "path" "path/filepath" "strings" + "syscall" ) var ( - hostname = flag.String("hostname", "localhost", "Hostname") - bind = flag.String("bind", ":6667", "Address to bind to") - motd = flag.String("motd", "", "Path to MOTD file") - logdir = flag.String("logdir", "", "Absolute path to directory for logs") - statedir = flag.String("statedir", "", "Absolute path to directory for states") + hostname = flag.String("hostname", "localhost", "Hostname") + bind = flag.String("bind", ":6667", "Address to bind to") + motd = flag.String("motd", "", "Path to MOTD file") + logdir = flag.String("logdir", "", "Absolute path to directory for logs") + statedir = flag.String("statedir", "", "Absolute path to directory for states") + passwords = flag.String("passwords", "", "Optional path to passwords file") tlsKey = flag.String("tls_key", "", "TLS keyfile") tlsCert = flag.String("tls_cert", "", "TLS certificate") @@ -118,6 +122,18 @@ func Run() { } log.Println("Listening on", *bind) + if *passwords != "" { + daemon.PasswordsRefresh() + hups := make(chan os.Signal) + signal.Notify(hups, syscall.SIGHUP) + go func() { + for { + <-hups + daemon.PasswordsRefresh() + } + }() + } + go daemon.Processor(events) for { conn, err := listener.Accept() diff --git a/room.go b/room.go index 3f8901c..eeff4d0 100644 --- a/room.go +++ b/room.go @@ -46,6 +46,10 @@ type Room struct { stateSink chan<- StateEvent } +func (r Room) String() string { + return r.name +} + func NewRoom(hostname, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room { room := Room{name: name} room.members = make(map[*Client]bool)