X-Git-Url: http://www.git.cypherpunks.ru/?p=goircd.git;a=blobdiff_plain;f=client.go;h=6cacbdb81ac27fa248ca3aa474b30ed602aeaacc;hp=1b4a9c365993abc7aa2b0f537bd98b498fe87947;hb=HEAD;hpb=8702ace766119effc2c2ec4afa284bec6f24c4da diff --git a/client.go b/client.go index 1b4a9c3..6cacbdb 100644 --- a/client.go +++ b/client.go @@ -1,125 +1,457 @@ -/* -goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014 Sergey Matveev - -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, either version 3 of the License, or -(at your option) any later version. - -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 . -*/ +// goircd -- minimalistic simple Internet Relay Chat (IRC) server +// Copyright (C) 2014-2024 Sergey Matveev +// +// 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 . + package main import ( "bytes" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "fmt" "log" "net" + "os" + "regexp" + "sort" "strings" + "sync" "time" ) const ( - CRLF = "\x0d\x0a" - BufSize = 1380 + 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 { - hostname *string - conn net.Conn - registered bool - nickname string - username string - realname string - password string - away *string + 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 } -type ClientAlivenessState struct { - pingSent bool - timestamp time.Time +func (c *Client) String() string { + return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "") } -func (client Client) String() string { - return client.nickname + "!" + client.username + "@" + client.conn.RemoteAddr().String() +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 NewClient(hostname *string, conn net.Conn) *Client { - return &Client{hostname: hostname, conn: conn, nickname: "*", password: ""} +func (c *Client) Close() { + c.Lock() + if c.alive { + close(c.outBuf) + c.alive = false + } + c.Unlock() } -// Client processor blockingly reads everything remote client sends, -// splits messages by CRLF and send them to Daemon gorouting for processing -// it futher. Also it can signalize that client is unavailable (disconnected). -func (client *Client) Processor(sink chan<- ClientEvent) { - var bufNet []byte - buf := make([]byte, 0) - log.Println(client, "New client") - sink <- ClientEvent{client, EventNew, ""} +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 { - bufNet = make([]byte, BufSize) - _, err := client.conn.Read(bufNet) + if prev == BufSize { + log.Println(c, "input buffer size exceeded, kicking him") + break + } + n, err = c.conn.Read(buf[prev:]) if err != nil { - sink <- ClientEvent{client, EventDel, ""} break } - bufNet = bytes.TrimRight(bufNet, "\x00") - buf = append(buf, bufNet...) - if !bytes.HasSuffix(buf, []byte(CRLF)) { + prev += n + CheckMore: + i = bytes.Index(buf[:prev], CRLF) + if i == -1 { continue } - for _, msg := range bytes.Split(buf[:len(buf)-2], []byte(CRLF)) { - if len(msg) > 0 { - sink <- ClientEvent{client, EventMsg, string(msg)} + 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 } - buf = []byte{} } + c.conn.Close() + clientsWG.Done() } -// Send message as is with CRLF appended. -func (client *Client) Msg(text string) { - client.conn.Write([]byte(text + CRLF)) +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 } -// Send message from server. It has ": servername" prefix. -func (client *Client) Reply(text string) { - client.Msg(":" + *client.hostname + " " + text) +func (c *Client) Reply(text string) { + c.Msg(":" + *hostname + " " + text) } -// Send server message, concatenating all provided text parts and -// prefix the last one with ":". -func (client *Client) ReplyParts(code string, text ...string) { - parts := []string{code} - for _, t := range text { - parts = append(parts, t) - } +func (c *Client) ReplyParts(code string, text ...string) { + parts := append([]string{code}, text...) parts[len(parts)-1] = ":" + parts[len(parts)-1] - client.Reply(strings.Join(parts, " ")) + 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 := os.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") } -// Send nicknamed server message. After servername it always has target -// client's nickname. The last part is prefixed with ":". -func (client *Client) ReplyNicknamed(code string, text ...string) { - client.ReplyParts(code, append([]string{client.nickname}, text...)...) +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, ""} + } } -// Reply "461 not enough parameters" error for given command. -func (client *Client) ReplyNotEnoughParameters(command string) { - client.ReplyNicknamed("461", command, "Not enough parameters") +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") + } } -// Reply "403 no such channel" error for specified channel. -func (client *Client) ReplyNoChannel(channel string) { - client.ReplyNicknamed("403", channel, "No such channel") +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 (client *Client) ReplyNoNickChan(channel string) { - client.ReplyNicknamed("401", channel, "No such nick/channel") +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 := os.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") + } + } }