* 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:
SUPPORTED IRC COMMANDS
-* NICK/USER during registration workflow
+* PASS/NICK/USER during registration workflow
* PING/PONGs
* NOTICE/PRIVMSG
* MOTD, LUSERS, WHO, WHOIS, QUIT
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
nickname string
username string
realname string
+ password string
}
type ClientAlivenessState struct {
}
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,
bufNet = make([]byte, BufSize)
_, err := client.conn.Read(bufNet)
if err != nil {
- log.Println(client, "connection lost", err)
sink <- ClientEvent{client, EventDel, ""}
break
}
"regexp"
"sort"
"strings"
+ "sync"
"time"
)
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,9}$")
)
+var passwordsRefreshLock sync.Mutex
+
type Daemon struct {
Verbose bool
hostname string
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 {
// 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")
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")
client.ReplyNicknamed("004", daemon.hostname+" goircd o o")
daemon.SendLusers(client)
daemon.SendMotd(client)
+ log.Println(client, "logged in")
}
}
continue
}
roomNew, roomSink := daemon.RoomRegister(room)
+ log.Println("Room", roomNew, "created")
if key != "" {
roomNew.key = key
roomNew.StateSave()
log.Println(client, "command", command)
}
if command == "QUIT" {
+ log.Println(client, "quit")
delete(daemon.clients, client)
delete(daemon.clientAliveness, client)
client.conn.Close()
}
}
}
+
+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()
+}
"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")
}
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()
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)