b7fb219307
* NAMES and WALLOPS command * -cloak option * -v renamed to -verbose * -passwords renamed to -passwd * -debug option prints traffic messages * without -verbose only startup/shutdown and error messages are printed * -timestamped option provides timestamps in printed messages, as earlier. No timestamps is useful for running under daemontools * passwords are stored in SHA256-hashed format * state files replaced with state directory with files * removed many unnecessary pointers and locks * graceful shutdown with all clients notification * fixed time structure printing in log files, instead of short human readable timestamp * PART is sent to the user itself, to notify his client about leaving * log messages are printed to stdout, instead of stderr, for friendliness with daemontools logger * ability to configure newly created directories and files with -perm-state-dir, -perm-state-file, -perm-log-file
463 lines
10 KiB
Go
463 lines
10 KiB
Go
/*
|
|
goircd -- minimalistic simple Internet Relay Chat (IRC) server
|
|
Copyright (C) 2014-2020 Sergey Matveev <stargrave@stargrave.org>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, version 3 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
BufSize = 1500
|
|
MaxOutBuf = 128
|
|
)
|
|
|
|
var (
|
|
CRLF []byte = []byte{'\x0d', '\x0a'}
|
|
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
|
|
|
|
clients map[*Client]struct{} = make(map[*Client]struct{})
|
|
clientsLock sync.RWMutex
|
|
clientsWG sync.WaitGroup
|
|
)
|
|
|
|
type Client struct {
|
|
conn net.Conn
|
|
registered bool
|
|
nickname string
|
|
username string
|
|
realname string
|
|
password string
|
|
away string
|
|
recvTimestamp time.Time
|
|
sendTimestamp time.Time
|
|
outBuf chan string
|
|
alive bool
|
|
sync.Mutex
|
|
}
|
|
|
|
func (c *Client) Host() string {
|
|
if *cloak != "" {
|
|
return *cloak
|
|
}
|
|
addr := c.conn.RemoteAddr().String()
|
|
if host, _, err := net.SplitHostPort(addr); err == nil {
|
|
addr = host
|
|
}
|
|
if domains, err := net.LookupAddr(addr); err == nil {
|
|
addr = strings.TrimSuffix(domains[0], ".")
|
|
}
|
|
return addr
|
|
}
|
|
|
|
func (c *Client) String() string {
|
|
return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
|
|
}
|
|
|
|
func NewClient(conn net.Conn, events chan ClientEvent) *Client {
|
|
c := Client{
|
|
conn: conn,
|
|
nickname: "*",
|
|
username: "",
|
|
recvTimestamp: time.Now(),
|
|
sendTimestamp: time.Now(),
|
|
alive: true,
|
|
outBuf: make(chan string, MaxOutBuf),
|
|
}
|
|
clientsWG.Add(2)
|
|
go c.MsgSender()
|
|
go c.Processor(events)
|
|
return &c
|
|
}
|
|
|
|
func (c *Client) Close() {
|
|
c.Lock()
|
|
if c.alive {
|
|
close(c.outBuf)
|
|
c.alive = false
|
|
}
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) Processor(events chan ClientEvent) {
|
|
events <- ClientEvent{c, EventNew, ""}
|
|
if *verbose {
|
|
log.Println(c, "connected")
|
|
}
|
|
buf := make([]byte, BufSize*2)
|
|
var n, prev, i int
|
|
var msg string
|
|
var err error
|
|
for {
|
|
if prev == BufSize {
|
|
log.Println(c, "input buffer size exceeded, kicking him")
|
|
break
|
|
}
|
|
n, err = c.conn.Read(buf[prev:])
|
|
if err != nil {
|
|
break
|
|
}
|
|
prev += n
|
|
CheckMore:
|
|
i = bytes.Index(buf[:prev], CRLF)
|
|
if i == -1 {
|
|
continue
|
|
}
|
|
if *debug {
|
|
log.Println(c, "<-", msg)
|
|
}
|
|
msg = string(buf[:i])
|
|
if *debug {
|
|
log.Println(c, "->", msg)
|
|
}
|
|
events <- ClientEvent{c, EventMsg, msg}
|
|
copy(buf, buf[i+2:prev])
|
|
prev -= (i + 2)
|
|
goto CheckMore
|
|
}
|
|
c.Close()
|
|
if *verbose {
|
|
log.Println(c, "disconnected")
|
|
}
|
|
events <- ClientEvent{c, EventDel, ""}
|
|
clientsWG.Done()
|
|
}
|
|
|
|
func (c *Client) MsgSender() {
|
|
var err error
|
|
for msg := range c.outBuf {
|
|
if *debug {
|
|
log.Println(c, "<-", msg)
|
|
}
|
|
if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
|
|
if *verbose {
|
|
log.Println(c, "error writing", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
c.conn.Close()
|
|
clientsWG.Done()
|
|
}
|
|
|
|
func (c *Client) Msg(text string) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if !c.alive {
|
|
return
|
|
}
|
|
if len(c.outBuf) == MaxOutBuf {
|
|
log.Println(c, "output buffer size exceeded, kicking him")
|
|
if c.alive {
|
|
close(c.outBuf)
|
|
c.alive = false
|
|
}
|
|
return
|
|
}
|
|
c.outBuf <- text
|
|
}
|
|
|
|
func (c *Client) Reply(text string) {
|
|
c.Msg(":" + *hostname + " " + text)
|
|
}
|
|
|
|
func (c *Client) ReplyParts(code string, text ...string) {
|
|
parts := []string{code}
|
|
for _, t := range text {
|
|
parts = append(parts, t)
|
|
}
|
|
parts[len(parts)-1] = ":" + parts[len(parts)-1]
|
|
c.Reply(strings.Join(parts, " "))
|
|
}
|
|
|
|
func (c *Client) ReplyNicknamed(code string, text ...string) {
|
|
c.ReplyParts(code, append([]string{c.nickname}, text...)...)
|
|
}
|
|
|
|
func (c *Client) ReplyNotEnoughParameters(command string) {
|
|
c.ReplyNicknamed("461", command, "Not enough parameters")
|
|
}
|
|
|
|
func (c *Client) ReplyNoChannel(channel string) {
|
|
c.ReplyNicknamed("403", channel, "No such channel")
|
|
}
|
|
|
|
func (c *Client) ReplyNoNickChan(channel string) {
|
|
c.ReplyNicknamed("401", channel, "No such nick/channel")
|
|
}
|
|
|
|
func (c *Client) SendLusers() {
|
|
lusers := 0
|
|
clientsLock.RLock()
|
|
for client := range clients {
|
|
if client.registered {
|
|
lusers++
|
|
}
|
|
}
|
|
clientsLock.RUnlock()
|
|
c.ReplyNicknamed(
|
|
"251",
|
|
fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
|
|
lusers,
|
|
))
|
|
}
|
|
|
|
func (c *Client) SendMotd() {
|
|
if motd == nil {
|
|
c.ReplyNicknamed("422", "MOTD File is missing")
|
|
return
|
|
}
|
|
motdText, err := ioutil.ReadFile(*motd)
|
|
if err != nil {
|
|
log.Printf("can not read motd file %s: %v", *motd, err)
|
|
c.ReplyNicknamed("422", "Error reading MOTD File")
|
|
return
|
|
}
|
|
c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
|
|
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
|
|
c.ReplyNicknamed("372", "- "+s)
|
|
}
|
|
c.ReplyNicknamed("376", "End of /MOTD command")
|
|
}
|
|
|
|
func (c *Client) Join(cmd string) {
|
|
args := strings.Split(cmd, " ")
|
|
rs := strings.Split(args[0], ",")
|
|
keys := []string{}
|
|
if len(args) > 1 {
|
|
keys = strings.Split(args[1], ",")
|
|
}
|
|
RoomCycle:
|
|
for n, roomName := range rs {
|
|
if !RERoom.MatchString(roomName) {
|
|
c.ReplyNoChannel(roomName)
|
|
continue
|
|
}
|
|
var key string
|
|
if (n < len(keys)) && (keys[n] != "") {
|
|
key = keys[n]
|
|
}
|
|
roomsLock.RLock()
|
|
for roomNameExisting, room := range rooms {
|
|
if roomName != roomNameExisting {
|
|
continue
|
|
}
|
|
if (room.key != "") && (room.key != key) {
|
|
c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
|
|
roomsLock.RUnlock()
|
|
return
|
|
}
|
|
room.events <- ClientEvent{c, EventNew, ""}
|
|
roomsLock.RUnlock()
|
|
continue RoomCycle
|
|
}
|
|
roomsLock.RUnlock()
|
|
roomNew := RoomRegister(roomName)
|
|
if *verbose {
|
|
log.Println("room", roomName, "created")
|
|
}
|
|
if key != "" {
|
|
roomNew.key = key
|
|
roomNew.StateSave()
|
|
}
|
|
roomNew.events <- ClientEvent{c, EventNew, ""}
|
|
}
|
|
}
|
|
|
|
func (client *Client) SendWhois(nicknames []string) {
|
|
var c *Client
|
|
for _, nickname := range nicknames {
|
|
nickname = strings.ToLower(nickname)
|
|
clientsLock.RLock()
|
|
for c = range clients {
|
|
if strings.ToLower(c.nickname) == nickname {
|
|
clientsLock.RUnlock()
|
|
goto Found
|
|
}
|
|
}
|
|
clientsLock.RUnlock()
|
|
client.ReplyNoNickChan(nickname)
|
|
continue
|
|
Found:
|
|
var host string
|
|
if *cloak != "" {
|
|
host = *cloak
|
|
} else {
|
|
host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
|
|
if err != nil {
|
|
log.Printf("can't parse RemoteAddr %q: %v", host, err)
|
|
host = "Unknown"
|
|
}
|
|
}
|
|
client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
|
|
client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
|
|
if c.away != "" {
|
|
client.ReplyNicknamed("301", c.nickname, c.away)
|
|
}
|
|
subscriptions := make([]string, 0)
|
|
roomsLock.RLock()
|
|
for _, room := range rooms {
|
|
for subscriber := range room.members {
|
|
if subscriber.nickname == nickname {
|
|
subscriptions = append(subscriptions, room.name)
|
|
}
|
|
}
|
|
}
|
|
roomsLock.RUnlock()
|
|
sort.Strings(subscriptions)
|
|
client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
|
|
client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
|
|
}
|
|
}
|
|
|
|
func (c *Client) SendList(cols []string) {
|
|
var rs []string
|
|
if (len(cols) > 1) && (cols[1] != "") {
|
|
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
|
|
} else {
|
|
rs = make([]string, 0)
|
|
roomsLock.RLock()
|
|
for r := range rooms {
|
|
rs = append(rs, r)
|
|
}
|
|
roomsLock.RUnlock()
|
|
}
|
|
sort.Strings(rs)
|
|
roomsLock.RLock()
|
|
for _, r := range rs {
|
|
if room, found := rooms[r]; found {
|
|
c.ReplyNicknamed(
|
|
"322",
|
|
r,
|
|
fmt.Sprintf("%d", len(room.members)),
|
|
room.topic,
|
|
)
|
|
}
|
|
}
|
|
roomsLock.RUnlock()
|
|
c.ReplyNicknamed("323", "End of /LIST")
|
|
}
|
|
|
|
func (c *Client) Register(cmd string, cols []string) {
|
|
switch cmd {
|
|
case "PASS":
|
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
|
c.ReplyNotEnoughParameters("PASS")
|
|
return
|
|
}
|
|
password := strings.TrimPrefix(cols[1], ":")
|
|
c.password = password
|
|
case "NICK":
|
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
|
c.ReplyParts("431", "No nickname given")
|
|
return
|
|
}
|
|
nickname := cols[1]
|
|
// Compatibility with some clients prepending colons to nickname
|
|
nickname = strings.TrimPrefix(nickname, ":")
|
|
nickname = strings.ToLower(nickname)
|
|
if !RENickname.MatchString(nickname) {
|
|
c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
|
|
return
|
|
}
|
|
clientsLock.RLock()
|
|
for existingClient := range clients {
|
|
if existingClient.nickname == nickname {
|
|
clientsLock.RUnlock()
|
|
c.ReplyParts("433", "*", nickname, "Nickname is already in use")
|
|
return
|
|
}
|
|
}
|
|
clientsLock.RUnlock()
|
|
c.nickname = nickname
|
|
case "USER":
|
|
if len(cols) == 1 {
|
|
c.ReplyNotEnoughParameters("USER")
|
|
return
|
|
}
|
|
args := strings.SplitN(cols[1], " ", 4)
|
|
if len(args) < 4 {
|
|
c.ReplyNotEnoughParameters("USER")
|
|
return
|
|
}
|
|
c.username = args[0]
|
|
realname := strings.TrimLeft(args[3], ":")
|
|
c.realname = realname
|
|
}
|
|
if c.nickname != "*" && c.username != "" {
|
|
if *passwords != "" {
|
|
authenticated := false
|
|
if c.password == "" {
|
|
c.ReplyParts("462", "You may not register")
|
|
c.Close()
|
|
return
|
|
}
|
|
contents, err := ioutil.ReadFile(*passwords)
|
|
if err != nil {
|
|
log.Fatalf("can not read passwords file %s: %s", *passwords, err)
|
|
return
|
|
}
|
|
for n, entry := range strings.Split(string(contents), "\n") {
|
|
if entry == "" || strings.HasPrefix(entry, "#") {
|
|
continue
|
|
}
|
|
cols := strings.Split(entry, ":")
|
|
if len(cols) != 2 {
|
|
log.Fatalf("bad passwords format: %s:%d", *passwords, n)
|
|
continue
|
|
}
|
|
if cols[0] != c.nickname {
|
|
continue
|
|
}
|
|
h := sha256.Sum256([]byte(c.password))
|
|
authenticated = subtle.ConstantTimeCompare(
|
|
[]byte(hex.EncodeToString(h[:])),
|
|
[]byte(cols[1]),
|
|
) == 1
|
|
break
|
|
}
|
|
if !authenticated {
|
|
c.ReplyParts("462", "You may not register")
|
|
c.Close()
|
|
return
|
|
}
|
|
}
|
|
c.registered = true
|
|
c.ReplyNicknamed("001", "Hi, welcome to IRC")
|
|
c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
|
|
c.ReplyNicknamed("003", "This server was created sometime")
|
|
c.ReplyNicknamed("004", *hostname+" goircd o o")
|
|
c.SendLusers()
|
|
c.SendMotd()
|
|
if *verbose {
|
|
log.Println(c, "logged in")
|
|
}
|
|
}
|
|
}
|