Ability to authenticate users by nickname↔password database

This commit is contained in:
Sergey Matveev 2014-08-14 14:01:54 +04:00
parent 5c79c4235e
commit a939b027fd
5 changed files with 83 additions and 8 deletions

17
README
View File

@ -28,6 +28,7 @@ But it has some convincing features:
* Optional permanent channel's state saving in plain text files * Optional permanent channel's state saving in plain text files
(so you can reload daemon and all channels topics and keys won't (so you can reload daemon and all channels topics and keys won't
disappear) disappear)
* Optional ability to authenticate users by nickname↔password
Some remarks and recommendations related to it's simplicity: 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 SUPPORTED IRC COMMANDS
* NICK/USER during registration workflow * PASS/NICK/USER during registration workflow
* PING/PONGs * PING/PONGs
* NOTICE/PRIVMSG * NOTICE/PRIVMSG
* MOTD, LUSERS, WHO, WHOIS, QUIT * 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 loaded during startup. If omitted, then states will be
lost after daemon termination lost after daemon termination
* -tls_key/-tls_cert: enable TLS and specify key and certificate file * -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 * -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 LICENCE
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify

View File

@ -37,6 +37,7 @@ type Client struct {
nickname string nickname string
username string username string
realname string realname string
password string
} }
type ClientAlivenessState struct { type ClientAlivenessState struct {
@ -49,7 +50,7 @@ func (client Client) String() string {
} }
func NewClient(hostname string, conn net.Conn) *Client { 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, // Client processor blockingly reads everything remote client sends,
@ -64,7 +65,6 @@ func (client *Client) Processor(sink chan<- ClientEvent) {
bufNet = make([]byte, BufSize) bufNet = make([]byte, BufSize)
_, err := client.conn.Read(bufNet) _, err := client.conn.Read(bufNet)
if err != nil { if err != nil {
log.Println(client, "connection lost", err)
sink <- ClientEvent{client, EventDel, ""} sink <- ClientEvent{client, EventDel, ""}
break break
} }

View File

@ -25,6 +25,7 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
@ -38,6 +39,8 @@ var (
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,9}$") RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,9}$")
) )
var passwordsRefreshLock sync.Mutex
type Daemon struct { type Daemon struct {
Verbose bool Verbose bool
hostname string hostname string
@ -49,6 +52,7 @@ type Daemon struct {
lastAlivenessCheck time.Time lastAlivenessCheck time.Time
logSink chan<- LogEvent logSink chan<- LogEvent
stateSink chan<- StateEvent stateSink chan<- StateEvent
passwords map[string]string
} }
func NewDaemon(hostname, motd string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Daemon { 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. // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) { func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) {
switch command { switch command {
case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PASS")
return
}
client.password = cols[1]
case "NICK": case "NICK":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyParts("431", "No nickname given") 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], ":") client.realname = strings.TrimLeft(args[3], ":")
} }
if client.nickname != "*" && client.username != "" { 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.registered = true
client.ReplyNicknamed("001", "Hi, welcome to IRC") client.ReplyNicknamed("001", "Hi, welcome to IRC")
client.ReplyNicknamed("002", "Your host is "+daemon.hostname+", running goircd") 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") client.ReplyNicknamed("004", daemon.hostname+" goircd o o")
daemon.SendLusers(client) daemon.SendLusers(client)
daemon.SendMotd(client) daemon.SendMotd(client)
log.Println(client, "logged in")
} }
} }
@ -247,6 +266,7 @@ func (daemon *Daemon) HandlerJoin(client *Client, cmd string) {
continue continue
} }
roomNew, roomSink := daemon.RoomRegister(room) roomNew, roomSink := daemon.RoomRegister(room)
log.Println("Room", roomNew, "created")
if key != "" { if key != "" {
roomNew.key = key roomNew.key = key
roomNew.StateSave() roomNew.StateSave()
@ -302,6 +322,7 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
log.Println(client, "command", command) log.Println(client, "command", command)
} }
if command == "QUIT" { if command == "QUIT" {
log.Println(client, "quit")
delete(daemon.clients, client) delete(daemon.clients, client)
delete(daemon.clientAliveness, client) delete(daemon.clientAliveness, client)
client.conn.Close() 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()
}

View File

@ -23,17 +23,21 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"os"
"os/signal"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
) )
var ( var (
hostname = flag.String("hostname", "localhost", "Hostname") hostname = flag.String("hostname", "localhost", "Hostname")
bind = flag.String("bind", ":6667", "Address to bind to") bind = flag.String("bind", ":6667", "Address to bind to")
motd = flag.String("motd", "", "Path to MOTD file") motd = flag.String("motd", "", "Path to MOTD file")
logdir = flag.String("logdir", "", "Absolute path to directory for logs") logdir = flag.String("logdir", "", "Absolute path to directory for logs")
statedir = flag.String("statedir", "", "Absolute path to directory for states") 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") tlsKey = flag.String("tls_key", "", "TLS keyfile")
tlsCert = flag.String("tls_cert", "", "TLS certificate") tlsCert = flag.String("tls_cert", "", "TLS certificate")
@ -118,6 +122,18 @@ func Run() {
} }
log.Println("Listening on", *bind) 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) go daemon.Processor(events)
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()

View File

@ -46,6 +46,10 @@ type Room struct {
stateSink chan<- StateEvent stateSink chan<- StateEvent
} }
func (r Room) String() string {
return r.name
}
func NewRoom(hostname, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room { func NewRoom(hostname, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room {
room := Room{name: name} room := Room{name: name}
room.members = make(map[*Client]bool) room.members = make(map[*Client]bool)