]> Cypherpunks.ru repositories - goircd.git/commitdiff
Ability to authenticate users by nickname↔password database
authorSergey Matveev <stargrave@stargrave.org>
Thu, 14 Aug 2014 10:01:54 +0000 (14:01 +0400)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 14 Aug 2014 13:31:49 +0000 (17:31 +0400)
README
client.go
daemon.go
goircd.go
room.go

diff --git a/README b/README
index 03f31d88485e3ad1b1af0c04d05951c3c0731459..852e4557c7d88877b3ebf87ab2357d5a3b78f97d 100644 (file)
--- 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
index afd6312714837e70b8c89550575054ac5efd7341..4f48daf90be4f1f0981b96f6e30ca04fa1f438a0 100644 (file)
--- 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
                }
index 06916b0d6c39649dd4e5d519a72347fa72125ffd..ffd2ef1ffb06c059c4e52e9f76c753a500bbda09 100644 (file)
--- 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()
+}
index dbf6d7dd84384e182a9ee00f3290d4d107033db0..984b47b94144c134e7bfc57158b52de13dddbf84 100644 (file)
--- 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 3f8901caf894732e369863b8c514f6bfe62e84be..eeff4d06cb8580aced5945694a74bbdf8de5cb61 100644 (file)
--- 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)